]> git.dpolakovic.space - halo3-reclaimer/commitdiff
feat: Forge map extraction implemented 0.2.0
authorDavid Polakovic
Tue, 3 Nov 2026 08:56:20 +0000 (09:56 +0100)
committerDavid Polakovic
Tue, 3 Nov 2026 08:56:20 +0000 (09:56 +0100)
-new class MapReclaimer.java
-updated documentation and README
-corrected and unified the program version

60 files changed:
README.txt
build/Arbiter$Kind.class [new file with mode: 0644]
build/Arbiter.class
build/Cortana$1.class [new file with mode: 0644]
build/Cortana$Drive.class [new file with mode: 0644]
build/Cortana$Entry.class [new file with mode: 0644]
build/Cortana$Volume.class [new file with mode: 0644]
build/Cortana.class [new file with mode: 0644]
build/GraphicalUI$BrowseRawActionListener$1.class
build/GraphicalUI$BrowseRawActionListener.class
build/GraphicalUI$BrowseSaveActionListener.class
build/GraphicalUI$ReclaimActionListener$1.class
build/GraphicalUI$ReclaimActionListener.class
build/GraphicalUI.class
build/MapReclaimer$1.class [new file with mode: 0644]
build/MapReclaimer$MapInfo.class [new file with mode: 0644]
build/MapReclaimer$StfsEntry.class [new file with mode: 0644]
build/MapReclaimer$StfsPackage.class [new file with mode: 0644]
build/MapReclaimer.class
build/TerminalUI.class
build/mc.png
doc/Arbiter.html
doc/Cortana.html [new file with mode: 0644]
doc/ErrorHandler.html
doc/GraphicalUI.html
doc/Main.html
doc/MapReclaimer.html
doc/ScreenshotReclaimer.html
doc/TerminalUI.html
doc/allclasses-frame.html
doc/allclasses-noframe.html
doc/class-use/Arbiter.html
doc/class-use/Cortana.html [new file with mode: 0644]
doc/class-use/ErrorHandler.html
doc/class-use/GraphicalUI.html
doc/class-use/Main.html
doc/class-use/MapReclaimer.html
doc/class-use/ScreenshotReclaimer.html
doc/class-use/TerminalUI.html
doc/constant-values.html
doc/deprecated-list.html
doc/help-doc.html
doc/index-all.html
doc/index.html
doc/overview-summary.html
doc/overview-tree.html
doc/package-frame.html
doc/package-summary.html
doc/package-tree.html
doc/package-use.html
doc/serialized-form.html
doc/user-manual-editable.odt
doc/user-manual.pdf
halo3-reclaimer.jar
src/Arbiter.java
src/Cortana.java [new file with mode: 0644]
src/GraphicalUI.java
src/MapReclaimer.java
src/TerminalUI.java
src/mc.png

index bbb6493881d18462710b5984669cf603cefaa372..2b6ef61dd55b7d1dd39881a52e052268c07fe7f2 100755 (executable)
@@ -1,59 +1,75 @@
 Halo 3 Reclaimer
 player data extractor from Xbox 360 version of Halo 3 
-copyright 2025, David Polakovic, licensed under GPLv3
+copyright 2025, 2026, David Polakovic, licensed under GPLv3
+
 
 
 ABOUT:
 Today it is almost impossible to get you player data
 like screenshots or Forge map variants from classic 
 Xbox 360 Halo 3 to you PC. This program solves this 
-problem. 
+problem. The reclaimer currently exports:
+- player screenshots
+- Forge map variants
+
+
+
+BUILDING FORM SOURCE:
+  Even tough I am not a supporter of distributing software
+  in already built form, I do so for the comfort of target
+  audience of this specific program. So unless you want to
+  change some suff in the code, YOU CAN SKIP THIS PROCEDURE.
+  
+  Otherwise, there is a build.xml file configured for your
+  (and mine) convinience which requires build tool Apache Ant.
+  https://ant.apache.org/index.html
+  
+  To build the program:
+  1. Install Ant 
+  2. Inspect the 'build.xml' in root directory
+  3. Inspect the code in src to see what it does
+  4. Then simply run:
+           ant all
+
 
 
 HOW TO RUN:
-You need to have Java version 8 or later. For lanuching 
-the Reclaimer in graphical user interface mode, simply 
-double click the halo3-reclaimer.jar. If your GUI options 
-are limited, you can open it via terminal in terminal user 
-interface mode by typing:
-    java -jar halo3-reclaimer.jar -h
-Make sure to be in same directory as the file itself.
-NOTE: Apple users might experience security warnings as 
-for all .jar files. Check some tutorials how to open .jar 
-files on Mac or use terminal:
-    java -jar halo3-reclaimer.jar  (without option)
+  You need to have Java version 8 or later. For lanuching 
+  the Reclaimer in graphical user interface mode, simply 
+  double click the halo3-reclaimer.jar. If your GUI options 
+  are limited, you can open it via terminal in terminal user 
+  interface mode by typing:
+          java -jar halo3-reclaimer.jar -h
+  Make sure to be in same directory as the file itself.
+  NOTE: Apple users might experience security warnings as 
+  for all .jar files. Check some tutorials how to open .jar 
+  files on Mac or use terminal:
+          java -jar halo3-reclaimer.jar  (without option)
+
 
 
 HOW TO USE
-You can copy your Halo 3 screenshots and maps into 
-USB stick formated for Xbox 360 file system. Then, 
-insert the USB stick into the computer and run the 
-halo3-reclaimer.
-1. Click 'Browse' next to 'Raw file destination'
-   to select your Xbox 360 Halo 3 file
-2. Click 'Browse' next to 'Save destination
-   to choose where to save the converted image
-3. Click 'PELICAN!' to start the extraction process
-   The tool will convert your Xbox 360 file
-   into a standard JPEG image or map file* that you 
-   share on any device.
-* map files are not implemented yet
+  See user manual in 'Doc/user-manual.pdf'.
 
 
-BUILDING FORM SOURCE:
-There is a build.xml file configured for you convinience 
-which requires Apache Ant. Install Ant and simply run:
-    ant all
 
+LICENSE:
+  Halo 3 Reclaimer is intelectual property of David Polakovic
+  and is licensed under GNU General Public License version 3.
+  See full license in LICENSE.txt or on web: https://www.gnu.org/licenses/gpl-3.0.en.html
+
+  Documentation for the program is is intelectual property of
+  David Polakovic and is licensed under GNU Free Documentation License version 1.3 
+  See full license in DOC_LICENSE.txt or on web: https://www.gnu.org/licenses/fdl-1.3.html
 
 
-LICENSE:
-https://www.gnu.org/licenses/gpl-3.0.en.html
-see LICENSE.txt
 
 CONTACT:
-email@dpolakovic.space
-Fetch my GPG here: https://dpolakovic.space/gpg
+  email@dpolakovic.space
+  Fetch my GPG here: https://dpolakovic.space/gpg
+
+
 
 RELEASE NOTES:
-1.0 - Screenshots extracting
+  0.1 - Screenshots extracting 
+  0.2 - Forge maps extracting
diff --git a/build/Arbiter$Kind.class b/build/Arbiter$Kind.class
new file mode 100644 (file)
index 0000000..90195c3
Binary files /dev/null and b/build/Arbiter$Kind.class differ
index 4b6db2742e793cdf74db0a8adf42f669edabddc3..17cab3cbbfd4cb706909d8c7a29851076ff0da2b 100644 (file)
Binary files a/build/Arbiter.class and b/build/Arbiter.class differ
diff --git a/build/Cortana$1.class b/build/Cortana$1.class
new file mode 100644 (file)
index 0000000..e5d2c0e
Binary files /dev/null and b/build/Cortana$1.class differ
diff --git a/build/Cortana$Drive.class b/build/Cortana$Drive.class
new file mode 100644 (file)
index 0000000..e6e5178
Binary files /dev/null and b/build/Cortana$Drive.class differ
diff --git a/build/Cortana$Entry.class b/build/Cortana$Entry.class
new file mode 100644 (file)
index 0000000..ff65aaf
Binary files /dev/null and b/build/Cortana$Entry.class differ
diff --git a/build/Cortana$Volume.class b/build/Cortana$Volume.class
new file mode 100644 (file)
index 0000000..8dce265
Binary files /dev/null and b/build/Cortana$Volume.class differ
diff --git a/build/Cortana.class b/build/Cortana.class
new file mode 100644 (file)
index 0000000..54caa78
Binary files /dev/null and b/build/Cortana.class differ
index 63ab45205a32e77f8d239d062efcaeb0165e7d16..11b2217d785545948fbdc1cdc4f2c4b08dcd633f 100644 (file)
Binary files a/build/GraphicalUI$BrowseRawActionListener$1.class and b/build/GraphicalUI$BrowseRawActionListener$1.class differ
index 3012974aa7db3f0d774744742ead6d0da7d95238..5245384ed42d8cab43eec87e13837d12ee1d84ab 100644 (file)
Binary files a/build/GraphicalUI$BrowseRawActionListener.class and b/build/GraphicalUI$BrowseRawActionListener.class differ
index 8a71799305c36f1d193ee22795f62bf2ae64f477..18637bffea245988d83bb2134c12ff45b648548b 100644 (file)
Binary files a/build/GraphicalUI$BrowseSaveActionListener.class and b/build/GraphicalUI$BrowseSaveActionListener.class differ
index 2376c756659db34a1263dc544718379c6624a480..db8c726c1cf3a1152bb4fe4deb42f16cb7a173d0 100644 (file)
Binary files a/build/GraphicalUI$ReclaimActionListener$1.class and b/build/GraphicalUI$ReclaimActionListener$1.class differ
index 397b6367d9dd480a374853556d63b339a1af3304..8756b541f747fae9c4fa0f3daf51065939126124 100644 (file)
Binary files a/build/GraphicalUI$ReclaimActionListener.class and b/build/GraphicalUI$ReclaimActionListener.class differ
index cf805cad5b75376ac152329e6c68a4136464d1fb..4203d4e4a68edbc1929677b31920d0fd4bafcea8 100644 (file)
Binary files a/build/GraphicalUI.class and b/build/GraphicalUI.class differ
diff --git a/build/MapReclaimer$1.class b/build/MapReclaimer$1.class
new file mode 100644 (file)
index 0000000..7c73151
Binary files /dev/null and b/build/MapReclaimer$1.class differ
diff --git a/build/MapReclaimer$MapInfo.class b/build/MapReclaimer$MapInfo.class
new file mode 100644 (file)
index 0000000..677ce76
Binary files /dev/null and b/build/MapReclaimer$MapInfo.class differ
diff --git a/build/MapReclaimer$StfsEntry.class b/build/MapReclaimer$StfsEntry.class
new file mode 100644 (file)
index 0000000..a238425
Binary files /dev/null and b/build/MapReclaimer$StfsEntry.class differ
diff --git a/build/MapReclaimer$StfsPackage.class b/build/MapReclaimer$StfsPackage.class
new file mode 100644 (file)
index 0000000..12c5e3a
Binary files /dev/null and b/build/MapReclaimer$StfsPackage.class differ
index 0cf23b3f7104b8dfb1dfde170724646d70dbff72..65cfeb1829344f32b569909d1b76628d620fcc29 100644 (file)
Binary files a/build/MapReclaimer.class and b/build/MapReclaimer.class differ
index 80ef7fb36b8286b64bdb1bf43a500c21b3fb040f..602d2c331f84ae04277f4f3ee59a328c8556229b 100644 (file)
Binary files a/build/TerminalUI.class and b/build/TerminalUI.class differ
index a9e4591c7aa900ed9d5fa71cdf8749437a3d16b8..16d4afcb1d26ea40c668e0fb853c74922cef34bc 100644 (file)
Binary files a/build/mc.png and b/build/mc.png differ
index 9b6554416dd036b91d16198621581bbc9bd180d4..0e1d10dcc2b127a0ea611ab3ce1793684a6cf10a 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Arbiter (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -18,7 +18,7 @@
     catch(err) {
     }
 //-->
-var methods = {"i0":9,"i1":9};
+var methods = {"i0":9};
 var tabs = {65535:["t0","All Methods"],1:["t1","Static Methods"],8:["t4","Concrete Methods"]};
 var altColor = "altColor";
 var rowColor = "rowColor";
@@ -51,7 +51,7 @@ var activeTableTab = "activeTableTab";
 <div class="subNav">
 <ul class="navList">
 <li>Prev&nbsp;Class</li>
-<li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
 </ul>
 <ul class="navList">
 <li><a href="index.html?Arbiter.html" target="_top">Frames</a></li>
@@ -76,13 +76,13 @@ var activeTableTab = "activeTableTab";
 <ul class="subNavList">
 <li>Summary:&nbsp;</li>
 <li>Nested&nbsp;|&nbsp;</li>
-<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#field.summary">Field</a>&nbsp;|&nbsp;</li>
 <li><a href="#constructor.summary">Constr</a>&nbsp;|&nbsp;</li>
 <li><a href="#method.summary">Method</a></li>
 </ul>
 <ul class="subNavList">
 <li>Detail:&nbsp;</li>
-<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#field.detail">Field</a>&nbsp;|&nbsp;</li>
 <li><a href="#constructor.detail">Constr</a>&nbsp;|&nbsp;</li>
 <li><a href="#method.detail">Method</a></li>
 </ul>
@@ -112,10 +112,21 @@ var activeTableTab = "activeTableTab";
 <pre>public class <span class="typeNameLabel">Arbiter</span>
 extends java.lang.Object</pre>
 <div class="block">Arbiter class that determines file type and routes to appropriate extractor.
- 
- <p>This class analyzes the input file to determine whether it's a screenshot
- or map file, then calls the appropriate reclaimer. The Xbox 360 saves file 
- type directly in file name. So it just checks for names...</p></div>
+
+ <p>The input is a single file, a whole folder, or a USB stick set up by the
+ Xbox 360. A stick (or its Xbox360 folder, or one of its Data files) goes to
+ Cortana first, which copies the Halo 3 files out of the Xbox file system;
+ other files on the stick are ignored then.</p>
+
+ <p>Halo 3 keeps every screenshot and every Forge map in its own package,
+ so the Arbiter looks inside each file: a package holding
+ <code>screen.shot</code> is a screenshot, one holding <code>sandbox.map</code>
+ is a map. Screenshots are checked first, then maps. Anything else is skipped.
+ Only when the content says nothing (DDS, TGA, raw textures) the file name
+ decides, like in version 1.0.</p>
+
+ <p>Results go to two folders in the output directory:
+ <code>screenshots-reclaimed</code> and <code>maps-reclaimed</code>.</p></div>
 <dl>
 <dt><span class="simpleTagLabel">Version:</span></dt>
 <dd>1.0</dd>
@@ -126,6 +137,29 @@ extends java.lang.Object</pre>
 <div class="summary">
 <ul class="blockList">
 <li class="blockList">
+<!-- =========== FIELD SUMMARY =========== -->
+<ul class="blockList">
+<li class="blockList"><a name="field.summary">
+<!--   -->
+</a>
+<h3>Field Summary</h3>
+<table class="memberSummary" border="0" cellpadding="3" cellspacing="0" summary="Field Summary table, listing fields, and an explanation">
+<caption><span>Fields</span><span class="tabEnd">&nbsp;</span></caption>
+<tr>
+<th class="colFirst" scope="col">Modifier and Type</th>
+<th class="colLast" scope="col">Field and Description</th>
+</tr>
+<tr class="altColor">
+<td class="colFirst"><code>static java.lang.String</code></td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Arbiter.html#MAPS_DIR">MAPS_DIR</a></span></code>&nbsp;</td>
+</tr>
+<tr class="rowColor">
+<td class="colFirst"><code>static java.lang.String</code></td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Arbiter.html#SCREENSHOTS_DIR">SCREENSHOTS_DIR</a></span></code>&nbsp;</td>
+</tr>
+</table>
+</li>
+</ul>
 <!-- ======== CONSTRUCTOR SUMMARY ======== -->
 <ul class="blockList">
 <li class="blockList"><a name="constructor.summary">
@@ -157,12 +191,10 @@ extends java.lang.Object</pre>
 </tr>
 <tr id="i0" class="altColor">
 <td class="colFirst"><code>static java.lang.String</code></td>
-<td class="colLast"><code><span class="memberNameLink"><a href="Arbiter.html#getFileTypeDescription-java.io.File-">getFileTypeDescription</a></span>(java.io.File&nbsp;inputFile)</code>&nbsp;</td>
-</tr>
-<tr id="i1" class="rowColor">
-<td class="colFirst"><code>static void</code></td>
-<td class="colLast"><code><span class="memberNameLink"><a href="Arbiter.html#processFile-java.io.File-java.io.File-">processFile</a></span>(java.io.File&nbsp;inputFile,
-           java.io.File&nbsp;outputDir)</code>&nbsp;</td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Arbiter.html#processFile-java.io.File-java.io.File-">processFile</a></span>(java.io.File&nbsp;input,
+           java.io.File&nbsp;outputDir)</code>
+<div class="block">Reclaims all screenshots and maps found in a file or folder.</div>
+</td>
 </tr>
 </table>
 <ul class="blockList">
@@ -180,6 +212,40 @@ extends java.lang.Object</pre>
 <div class="details">
 <ul class="blockList">
 <li class="blockList">
+<!-- ============ FIELD DETAIL =========== -->
+<ul class="blockList">
+<li class="blockList"><a name="field.detail">
+<!--   -->
+</a>
+<h3>Field Detail</h3>
+<a name="SCREENSHOTS_DIR">
+<!--   -->
+</a>
+<ul class="blockList">
+<li class="blockList">
+<h4>SCREENSHOTS_DIR</h4>
+<pre>public static final&nbsp;java.lang.String SCREENSHOTS_DIR</pre>
+<dl>
+<dt><span class="seeLabel">See Also:</span></dt>
+<dd><a href="constant-values.html#Arbiter.SCREENSHOTS_DIR">Constant Field Values</a></dd>
+</dl>
+</li>
+</ul>
+<a name="MAPS_DIR">
+<!--   -->
+</a>
+<ul class="blockListLast">
+<li class="blockList">
+<h4>MAPS_DIR</h4>
+<pre>public static final&nbsp;java.lang.String MAPS_DIR</pre>
+<dl>
+<dt><span class="seeLabel">See Also:</span></dt>
+<dd><a href="constant-values.html#Arbiter.MAPS_DIR">Constant Field Values</a></dd>
+</dl>
+</li>
+</ul>
+</li>
+</ul>
 <!-- ========= CONSTRUCTOR DETAIL ======== -->
 <ul class="blockList">
 <li class="blockList"><a name="constructor.detail">
@@ -206,27 +272,21 @@ extends java.lang.Object</pre>
 <a name="processFile-java.io.File-java.io.File-">
 <!--   -->
 </a>
-<ul class="blockList">
+<ul class="blockListLast">
 <li class="blockList">
 <h4>processFile</h4>
-<pre>public static&nbsp;void&nbsp;processFile(java.io.File&nbsp;inputFile,
-                               java.io.File&nbsp;outputDir)
-                        throws java.lang.Exception</pre>
+<pre>public static&nbsp;java.lang.String&nbsp;processFile(java.io.File&nbsp;input,
+                                           java.io.File&nbsp;outputDir)
+                                    throws java.lang.Exception</pre>
+<div class="block">Reclaims all screenshots and maps found in a file or folder.</div>
 <dl>
+<dt><span class="returnLabel">Returns:</span></dt>
+<dd>what was reclaimed, for example "Reclaimed 2 pictures and 1 map"</dd>
 <dt><span class="throwsLabel">Throws:</span></dt>
-<dd><code>java.lang.Exception</code></dd>
+<dd><code>java.lang.Exception</code> - if nothing could be reclaimed</dd>
 </dl>
 </li>
 </ul>
-<a name="getFileTypeDescription-java.io.File-">
-<!--   -->
-</a>
-<ul class="blockListLast">
-<li class="blockList">
-<h4>getFileTypeDescription</h4>
-<pre>public static&nbsp;java.lang.String&nbsp;getFileTypeDescription(java.io.File&nbsp;inputFile)</pre>
-</li>
-</ul>
 </li>
 </ul>
 </li>
@@ -257,7 +317,7 @@ extends java.lang.Object</pre>
 <div class="subNav">
 <ul class="navList">
 <li>Prev&nbsp;Class</li>
-<li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
 </ul>
 <ul class="navList">
 <li><a href="index.html?Arbiter.html" target="_top">Frames</a></li>
@@ -282,13 +342,13 @@ extends java.lang.Object</pre>
 <ul class="subNavList">
 <li>Summary:&nbsp;</li>
 <li>Nested&nbsp;|&nbsp;</li>
-<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#field.summary">Field</a>&nbsp;|&nbsp;</li>
 <li><a href="#constructor.summary">Constr</a>&nbsp;|&nbsp;</li>
 <li><a href="#method.summary">Method</a></li>
 </ul>
 <ul class="subNavList">
 <li>Detail:&nbsp;</li>
-<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#field.detail">Field</a>&nbsp;|&nbsp;</li>
 <li><a href="#constructor.detail">Constr</a>&nbsp;|&nbsp;</li>
 <li><a href="#method.detail">Method</a></li>
 </ul>
diff --git a/doc/Cortana.html b/doc/Cortana.html
new file mode 100644 (file)
index 0000000..65665a5
--- /dev/null
@@ -0,0 +1,336 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<!-- NewPage -->
+<html lang="en">
+<head>
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
+<title>Cortana (Halo 3 Reclaimer)</title>
+<meta name="date" content="2026-11-02">
+<link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
+<script type="text/javascript" src="script.js"></script>
+</head>
+<body>
+<script type="text/javascript"><!--
+    try {
+        if (location.href.indexOf('is-external=true') == -1) {
+            parent.document.title="Cortana (Halo 3 Reclaimer)";
+        }
+    }
+    catch(err) {
+    }
+//-->
+var methods = {"i0":9,"i1":9,"i2":9};
+var tabs = {65535:["t0","All Methods"],1:["t1","Static Methods"],8:["t4","Concrete Methods"]};
+var altColor = "altColor";
+var rowColor = "rowColor";
+var tableTab = "tableTab";
+var activeTableTab = "activeTableTab";
+</script>
+<noscript>
+<div>JavaScript is disabled on your browser.</div>
+</noscript>
+<!-- ========= START OF TOP NAVBAR ======= -->
+<div class="topNav"><a name="navbar.top">
+<!--   -->
+</a>
+<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
+<a name="navbar.top.firstrow">
+<!--   -->
+</a>
+<ul class="navList" title="Navigation">
+<li><a href="overview-summary.html">Overview</a></li>
+<li><a href="package-summary.html">Package</a></li>
+<li class="navBarCell1Rev">Class</li>
+<li><a href="class-use/Cortana.html">Use</a></li>
+<li><a href="package-tree.html">Tree</a></li>
+<li><a href="deprecated-list.html">Deprecated</a></li>
+<li><a href="index-all.html">Index</a></li>
+<li><a href="help-doc.html">Help</a></li>
+</ul>
+<div class="aboutLanguage"><b>Halo 3 Reclaimer</b></div>
+</div>
+<div class="subNav">
+<ul class="navList">
+<li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
+<li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
+</ul>
+<ul class="navList">
+<li><a href="index.html?Cortana.html" target="_top">Frames</a></li>
+<li><a href="Cortana.html" target="_top">No&nbsp;Frames</a></li>
+</ul>
+<ul class="navList" id="allclasses_navbar_top">
+<li><a href="allclasses-noframe.html">All&nbsp;Classes</a></li>
+</ul>
+<div>
+<script type="text/javascript"><!--
+  allClassesLink = document.getElementById("allclasses_navbar_top");
+  if(window==top) {
+    allClassesLink.style.display = "block";
+  }
+  else {
+    allClassesLink.style.display = "none";
+  }
+  //-->
+</script>
+</div>
+<div>
+<ul class="subNavList">
+<li>Summary:&nbsp;</li>
+<li>Nested&nbsp;|&nbsp;</li>
+<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#constructor.summary">Constr</a>&nbsp;|&nbsp;</li>
+<li><a href="#method.summary">Method</a></li>
+</ul>
+<ul class="subNavList">
+<li>Detail:&nbsp;</li>
+<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#constructor.detail">Constr</a>&nbsp;|&nbsp;</li>
+<li><a href="#method.detail">Method</a></li>
+</ul>
+</div>
+<a name="skip.navbar.top">
+<!--   -->
+</a></div>
+<!-- ========= END OF TOP NAVBAR ========= -->
+<!-- ======== START OF CLASS DATA ======== -->
+<div class="header">
+<h2 title="Class Cortana" class="title">Class Cortana</h2>
+</div>
+<div class="contentContainer">
+<ul class="inheritance">
+<li>java.lang.Object</li>
+<li>
+<ul class="inheritance">
+<li>Cortana</li>
+</ul>
+</li>
+</ul>
+<div class="description">
+<ul class="blockList">
+<li class="blockList">
+<hr>
+<br>
+<pre>public class <span class="typeNameLabel">Cortana</span>
+extends java.lang.Object</pre>
+<div class="block">Reader for USB sticks set up by the Xbox 360.
+
+ <p>The console doesn't store saves on the stick as normal files. It creates
+ a hidden folder <code>Xbox360</code> with big files <code>Data0000</code>,
+ <code>Data0001</code>, <code>Data0002</code>... Joined together they are one
+ virtual drive in the Xbox's own FATX file system. Cortana opens that drive,
+ walks to the Halo 3 folders (<code>Content\&lt;profile&gt;\4D5307E6\...</code>)
+ and copies their files into a temporary folder, where the Arbiter sorts them
+ out like any other files. Nothing on the stick is ever changed.</p>
+
+ <p>Only Cortana can understand these needlesly complicated Michaelsoft file
+ systems.</p>
+
+ <p>Partition offsets and the FATX layout follow Free60 and Velocity
+ (hetelek/Velocity, GPLv3).</p></div>
+<dl>
+<dt><span class="simpleTagLabel">Version:</span></dt>
+<dd>1.0</dd>
+</dl>
+</li>
+</ul>
+</div>
+<div class="summary">
+<ul class="blockList">
+<li class="blockList">
+<!-- ======== CONSTRUCTOR SUMMARY ======== -->
+<ul class="blockList">
+<li class="blockList"><a name="constructor.summary">
+<!--   -->
+</a>
+<h3>Constructor Summary</h3>
+<table class="memberSummary" border="0" cellpadding="3" cellspacing="0" summary="Constructor Summary table, listing constructors, and an explanation">
+<caption><span>Constructors</span><span class="tabEnd">&nbsp;</span></caption>
+<tr>
+<th class="colOne" scope="col">Constructor and Description</th>
+</tr>
+<tr class="altColor">
+<td class="colOne"><code><span class="memberNameLink"><a href="Cortana.html#Cortana--">Cortana</a></span>()</code>&nbsp;</td>
+</tr>
+</table>
+</li>
+</ul>
+<!-- ========== METHOD SUMMARY =========== -->
+<ul class="blockList">
+<li class="blockList"><a name="method.summary">
+<!--   -->
+</a>
+<h3>Method Summary</h3>
+<table class="memberSummary" border="0" cellpadding="3" cellspacing="0" summary="Method Summary table, listing methods, and an explanation">
+<caption><span id="t0" class="activeTableTab"><span>All Methods</span><span class="tabEnd">&nbsp;</span></span><span id="t1" class="tableTab"><span><a href="javascript:show(1);">Static Methods</a></span><span class="tabEnd">&nbsp;</span></span><span id="t4" class="tableTab"><span><a href="javascript:show(8);">Concrete Methods</a></span><span class="tabEnd">&nbsp;</span></span></caption>
+<tr>
+<th class="colFirst" scope="col">Modifier and Type</th>
+<th class="colLast" scope="col">Method and Description</th>
+</tr>
+<tr id="i0" class="altColor">
+<td class="colFirst"><code>static java.io.File</code></td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Cortana.html#copyHalo3Files-java.io.File-">copyHalo3Files</a></span>(java.io.File&nbsp;storage)</code>
+<div class="block">Copies every Halo 3 file from the stick into a new temporary folder and
+ returns that folder.</div>
+</td>
+</tr>
+<tr id="i1" class="rowColor">
+<td class="colFirst"><code>static void</code></td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Cortana.html#deleteCopies-java.io.File-">deleteCopies</a></span>(java.io.File&nbsp;folder)</code>
+<div class="block">Deletes the temporary folder made by copyHalo3Files().</div>
+</td>
+</tr>
+<tr id="i2" class="altColor">
+<td class="colFirst"><code>static java.io.File</code></td>
+<td class="colLast"><code><span class="memberNameLink"><a href="Cortana.html#findStorage-java.io.File-">findStorage</a></span>(java.io.File&nbsp;input)</code>
+<div class="block">Returns the Xbox360 folder when the input is a USB stick set up by the
+ console, its Xbox360 folder or one of its Data files.</div>
+</td>
+</tr>
+</table>
+<ul class="blockList">
+<li class="blockList"><a name="methods.inherited.from.class.java.lang.Object">
+<!--   -->
+</a>
+<h3>Methods inherited from class&nbsp;java.lang.Object</h3>
+<code>clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait</code></li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</div>
+<div class="details">
+<ul class="blockList">
+<li class="blockList">
+<!-- ========= CONSTRUCTOR DETAIL ======== -->
+<ul class="blockList">
+<li class="blockList"><a name="constructor.detail">
+<!--   -->
+</a>
+<h3>Constructor Detail</h3>
+<a name="Cortana--">
+<!--   -->
+</a>
+<ul class="blockListLast">
+<li class="blockList">
+<h4>Cortana</h4>
+<pre>public&nbsp;Cortana()</pre>
+</li>
+</ul>
+</li>
+</ul>
+<!-- ============ METHOD DETAIL ========== -->
+<ul class="blockList">
+<li class="blockList"><a name="method.detail">
+<!--   -->
+</a>
+<h3>Method Detail</h3>
+<a name="findStorage-java.io.File-">
+<!--   -->
+</a>
+<ul class="blockList">
+<li class="blockList">
+<h4>findStorage</h4>
+<pre>public static&nbsp;java.io.File&nbsp;findStorage(java.io.File&nbsp;input)</pre>
+<div class="block">Returns the Xbox360 folder when the input is a USB stick set up by the
+ console, its Xbox360 folder or one of its Data files. Otherwise null.</div>
+</li>
+</ul>
+<a name="copyHalo3Files-java.io.File-">
+<!--   -->
+</a>
+<ul class="blockList">
+<li class="blockList">
+<h4>copyHalo3Files</h4>
+<pre>public static&nbsp;java.io.File&nbsp;copyHalo3Files(java.io.File&nbsp;storage)
+                                   throws java.lang.Exception</pre>
+<div class="block">Copies every Halo 3 file from the stick into a new temporary folder and
+ returns that folder. Remove it with deleteCopies() when done.</div>
+<dl>
+<dt><span class="throwsLabel">Throws:</span></dt>
+<dd><code>java.lang.Exception</code> - if the stick can't be read</dd>
+</dl>
+</li>
+</ul>
+<a name="deleteCopies-java.io.File-">
+<!--   -->
+</a>
+<ul class="blockListLast">
+<li class="blockList">
+<h4>deleteCopies</h4>
+<pre>public static&nbsp;void&nbsp;deleteCopies(java.io.File&nbsp;folder)</pre>
+<div class="block">Deletes the temporary folder made by copyHalo3Files().</div>
+</li>
+</ul>
+</li>
+</ul>
+</li>
+</ul>
+</div>
+</div>
+<!-- ========= END OF CLASS DATA ========= -->
+<!-- ======= START OF BOTTOM NAVBAR ====== -->
+<div class="bottomNav"><a name="navbar.bottom">
+<!--   -->
+</a>
+<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
+<a name="navbar.bottom.firstrow">
+<!--   -->
+</a>
+<ul class="navList" title="Navigation">
+<li><a href="overview-summary.html">Overview</a></li>
+<li><a href="package-summary.html">Package</a></li>
+<li class="navBarCell1Rev">Class</li>
+<li><a href="class-use/Cortana.html">Use</a></li>
+<li><a href="package-tree.html">Tree</a></li>
+<li><a href="deprecated-list.html">Deprecated</a></li>
+<li><a href="index-all.html">Index</a></li>
+<li><a href="help-doc.html">Help</a></li>
+</ul>
+<div class="aboutLanguage"><i>File extractor from Xbox 360 version of Halo 3</i></div>
+</div>
+<div class="subNav">
+<ul class="navList">
+<li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
+<li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
+</ul>
+<ul class="navList">
+<li><a href="index.html?Cortana.html" target="_top">Frames</a></li>
+<li><a href="Cortana.html" target="_top">No&nbsp;Frames</a></li>
+</ul>
+<ul class="navList" id="allclasses_navbar_bottom">
+<li><a href="allclasses-noframe.html">All&nbsp;Classes</a></li>
+</ul>
+<div>
+<script type="text/javascript"><!--
+  allClassesLink = document.getElementById("allclasses_navbar_bottom");
+  if(window==top) {
+    allClassesLink.style.display = "block";
+  }
+  else {
+    allClassesLink.style.display = "none";
+  }
+  //-->
+</script>
+</div>
+<div>
+<ul class="subNavList">
+<li>Summary:&nbsp;</li>
+<li>Nested&nbsp;|&nbsp;</li>
+<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#constructor.summary">Constr</a>&nbsp;|&nbsp;</li>
+<li><a href="#method.summary">Method</a></li>
+</ul>
+<ul class="subNavList">
+<li>Detail:&nbsp;</li>
+<li>Field&nbsp;|&nbsp;</li>
+<li><a href="#constructor.detail">Constr</a>&nbsp;|&nbsp;</li>
+<li><a href="#method.detail">Method</a></li>
+</ul>
+</div>
+<a name="skip.navbar.bottom">
+<!--   -->
+</a></div>
+<!-- ======== END OF BOTTOM NAVBAR ======= -->
+<p class="legalCopy"><small>Copyright 2025. Documentation licensed under GFDL v1.3.</small></p>
+</body>
+</html>
index 879020a9bda9f4c71a009a19479783a0dd9c96f1..6ec8e75cd38e47d0108762b6380699813c56b8aa 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>ErrorHandler (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -50,7 +50,7 @@ var activeTableTab = "activeTableTab";
 </div>
 <div class="subNav">
 <ul class="navList">
-<li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
 <li><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
 </ul>
 <ul class="navList">
@@ -430,7 +430,7 @@ extends java.lang.Object</pre>
 </div>
 <div class="subNav">
 <ul class="navList">
-<li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Prev&nbsp;Class</span></a></li>
 <li><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Next&nbsp;Class</span></a></li>
 </ul>
 <ul class="navList">
index 0e22c05ad90644e8bf77c17fdd0f7057fb065d8a..94f7b5d9ec3e796e6f58108a8d7ac8b7e63e223f 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>GraphicalUI (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -146,7 +146,7 @@ extends javax.swing.JFrame</pre>
  logic for the application's windowed interface</p></div>
 <dl>
 <dt><span class="simpleTagLabel">Version:</span></dt>
-<dd>1.0</dd>
+<dd>1.1</dd>
 <dt><span class="seeLabel">See Also:</span></dt>
 <dd><a href="serialized-form.html#GraphicalUI">Serialized Form</a></dd>
 </dl>
index 68541aef9cb7fa63fcbe078428fb79a9fb2d42e5..711b5d2dd7a0bcf747e4d1f1a565152915afd98e 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Main (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 067f5d3c471a7f35d9ef15f48ebafba8c9f2b4bf..e50c908043a6476baf914933396f27300af91bca 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>MapReclaimer (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -112,11 +112,30 @@ var activeTableTab = "activeTableTab";
 <pre>public class <span class="typeNameLabel">MapReclaimer</span>
 extends java.lang.Object</pre>
 <div class="block">Core extraction engine for Forge map variants.
- 
- <p>This is harder than it looks. WIP.</p></div>
+
+ <p>Halo 3 saves every Forge map as its own Xbox 360 STFS package
+ (a CON file with a long hex name). The package holds one file called
+ <code>sandbox.map</code>. That file is the Forge map variant itself:
+ the object placements on top of one of the original maps, stored in
+ Bungie's BLF format (chunks <code>_blf</code>, <code>chdr</code>,
+ <code>mvar</code> and <code>_eof</code>).</p>
+
+ <p>Unlike screenshots, the map has no magic bytes worth scanning for,
+ so this class reads the STFS file system properly: it walks the file
+ table, follows the block chain of <code>sandbox.map</code> and checks
+ each block against the package hash tables.</p>
+
+ <p>Supported input formats:</p>
+ <ul>
+   <li>STFS container files (CON, LIVE, PIRS) holding a Halo 3 Forge map</li>
+   <li>already extracted <code>sandbox.map</code> files (BLF)</li>
+ </ul>
+
+ <p>The saved file is not converted in any way. It is byte for byte the
+ <code>sandbox.map</code> the console keeps inside the package.</p></div>
 <dl>
 <dt><span class="simpleTagLabel">Version:</span></dt>
-<dd>"1.0"</dd>
+<dd>1.0</dd>
 </dl>
 </li>
 </ul>
@@ -156,7 +175,10 @@ extends java.lang.Object</pre>
 <tr id="i0" class="altColor">
 <td class="colFirst"><code>void</code></td>
 <td class="colLast"><code><span class="memberNameLink"><a href="MapReclaimer.html#extractMap-java.io.File-java.io.File-">extractMap</a></span>(java.io.File&nbsp;inputFile,
-          java.io.File&nbsp;outputDir)</code>&nbsp;</td>
+          java.io.File&nbsp;outputDir)</code>
+<div class="block">Extracts the Forge map from an Xbox 360 file and saves it as
+ &lt;input name&gt;-reclaimed.map in the output directory.</div>
+</td>
 </tr>
 </table>
 <ul class="blockList">
@@ -206,9 +228,12 @@ extends java.lang.Object</pre>
 <pre>public&nbsp;void&nbsp;extractMap(java.io.File&nbsp;inputFile,
                        java.io.File&nbsp;outputDir)
                 throws java.lang.Exception</pre>
+<div class="block">Extracts the Forge map from an Xbox 360 file and saves it as
+ &lt;input name&gt;-reclaimed.map in the output directory.</div>
 <dl>
 <dt><span class="throwsLabel">Throws:</span></dt>
-<dd><code>java.lang.Exception</code></dd>
+<dd><code>java.lang.Exception</code> - if the file cannot be read, is not a Halo 3 Forge map,
+                   or is damaged</dd>
 </dl>
 </li>
 </ul>
index 4dfb1dbaf35867c0fd9bd04997d17c00deba65d4..f5d6983b3cc57af16f90ac936991adb99d32e7fb 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>ScreenshotReclaimer (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 76098a3eadee19daffcfa0da4d42a1ec1c9f043d..ebfd352d737b7dba3831cba2d82d02803ca9125e 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>TerminalUI (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 47b87ab8d90d9bd9c59198d42cc786ef71e380ab..9b46ad16e7d8ecc2a981025974f90b4ebe69e2d4 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>All Classes (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -13,6 +13,7 @@
 <div class="indexContainer">
 <ul>
 <li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;" target="classFrame">Arbiter</a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;" target="classFrame">Cortana</a></li>
 <li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;" target="classFrame">ErrorHandler</a></li>
 <li><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;" target="classFrame">GraphicalUI</a></li>
 <li><a href="Main.html" title="class in &lt;Unnamed&gt;" target="classFrame">Main</a></li>
index 1d07da3e989b271451085314be8d2417540cf88a..5f89623c308b59c2fba0135217876646d2f8d1d5 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>All Classes (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -13,6 +13,7 @@
 <div class="indexContainer">
 <ul>
 <li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></li>
 <li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;">ErrorHandler</a></li>
 <li><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;">GraphicalUI</a></li>
 <li><a href="Main.html" title="class in &lt;Unnamed&gt;">Main</a></li>
index 4ab3292c2766756954d9366959e41a859c8b70f1..98b442a7b1c8d10814356efd9711712e7ad412c8 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class Arbiter (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
diff --git a/doc/class-use/Cortana.html b/doc/class-use/Cortana.html
new file mode 100644 (file)
index 0000000..cc96b38
--- /dev/null
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<!-- NewPage -->
+<html lang="en">
+<head>
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
+<title>Uses of Class Cortana (Halo 3 Reclaimer)</title>
+<meta name="date" content="2026-11-02">
+<link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
+<script type="text/javascript" src="../script.js"></script>
+</head>
+<body>
+<script type="text/javascript"><!--
+    try {
+        if (location.href.indexOf('is-external=true') == -1) {
+            parent.document.title="Uses of Class Cortana (Halo 3 Reclaimer)";
+        }
+    }
+    catch(err) {
+    }
+//-->
+</script>
+<noscript>
+<div>JavaScript is disabled on your browser.</div>
+</noscript>
+<!-- ========= START OF TOP NAVBAR ======= -->
+<div class="topNav"><a name="navbar.top">
+<!--   -->
+</a>
+<div class="skipNav"><a href="#skip.navbar.top" title="Skip navigation links">Skip navigation links</a></div>
+<a name="navbar.top.firstrow">
+<!--   -->
+</a>
+<ul class="navList" title="Navigation">
+<li><a href="../overview-summary.html">Overview</a></li>
+<li><a href="../package-summary.html">Package</a></li>
+<li><a href="../Cortana.html" title="class in &lt;Unnamed&gt;">Class</a></li>
+<li class="navBarCell1Rev">Use</li>
+<li><a href="../overview-tree.html">Tree</a></li>
+<li><a href="../deprecated-list.html">Deprecated</a></li>
+<li><a href="../index-all.html">Index</a></li>
+<li><a href="../help-doc.html">Help</a></li>
+</ul>
+<div class="aboutLanguage"><b>Halo 3 Reclaimer</b></div>
+</div>
+<div class="subNav">
+<ul class="navList">
+<li>Prev</li>
+<li>Next</li>
+</ul>
+<ul class="navList">
+<li><a href="../index.html?class-use/Cortana.html" target="_top">Frames</a></li>
+<li><a href="Cortana.html" target="_top">No&nbsp;Frames</a></li>
+</ul>
+<ul class="navList" id="allclasses_navbar_top">
+<li><a href="../allclasses-noframe.html">All&nbsp;Classes</a></li>
+</ul>
+<div>
+<script type="text/javascript"><!--
+  allClassesLink = document.getElementById("allclasses_navbar_top");
+  if(window==top) {
+    allClassesLink.style.display = "block";
+  }
+  else {
+    allClassesLink.style.display = "none";
+  }
+  //-->
+</script>
+</div>
+<a name="skip.navbar.top">
+<!--   -->
+</a></div>
+<!-- ========= END OF TOP NAVBAR ========= -->
+<div class="header">
+<h2 title="Uses of Class Cortana" class="title">Uses of Class<br>Cortana</h2>
+</div>
+<div class="classUseContainer">No usage of Cortana</div>
+<!-- ======= START OF BOTTOM NAVBAR ====== -->
+<div class="bottomNav"><a name="navbar.bottom">
+<!--   -->
+</a>
+<div class="skipNav"><a href="#skip.navbar.bottom" title="Skip navigation links">Skip navigation links</a></div>
+<a name="navbar.bottom.firstrow">
+<!--   -->
+</a>
+<ul class="navList" title="Navigation">
+<li><a href="../overview-summary.html">Overview</a></li>
+<li><a href="../package-summary.html">Package</a></li>
+<li><a href="../Cortana.html" title="class in &lt;Unnamed&gt;">Class</a></li>
+<li class="navBarCell1Rev">Use</li>
+<li><a href="../overview-tree.html">Tree</a></li>
+<li><a href="../deprecated-list.html">Deprecated</a></li>
+<li><a href="../index-all.html">Index</a></li>
+<li><a href="../help-doc.html">Help</a></li>
+</ul>
+<div class="aboutLanguage"><i>File extractor from Xbox 360 version of Halo 3</i></div>
+</div>
+<div class="subNav">
+<ul class="navList">
+<li>Prev</li>
+<li>Next</li>
+</ul>
+<ul class="navList">
+<li><a href="../index.html?class-use/Cortana.html" target="_top">Frames</a></li>
+<li><a href="Cortana.html" target="_top">No&nbsp;Frames</a></li>
+</ul>
+<ul class="navList" id="allclasses_navbar_bottom">
+<li><a href="../allclasses-noframe.html">All&nbsp;Classes</a></li>
+</ul>
+<div>
+<script type="text/javascript"><!--
+  allClassesLink = document.getElementById("allclasses_navbar_bottom");
+  if(window==top) {
+    allClassesLink.style.display = "block";
+  }
+  else {
+    allClassesLink.style.display = "none";
+  }
+  //-->
+</script>
+</div>
+<a name="skip.navbar.bottom">
+<!--   -->
+</a></div>
+<!-- ======== END OF BOTTOM NAVBAR ======= -->
+<p class="legalCopy"><small>Copyright 2025. Documentation licensed under GFDL v1.3.</small></p>
+</body>
+</html>
index ec54bd067a0f426db789e8736374ddb6a61d940a..64136f5c3f2477f15ae9d75b5c6bcc94cef6ee9c 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class ErrorHandler (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index ddadcfc9ba2c449d3e056076a43a999596c0bcbb..afbd7efbb002ab6a382b69757ae53dc4a4914a7e 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class GraphicalUI (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index 3949df3020135512d4bd9d451bba1bc68ce3c35e..06f5dcabceb3165b888d0f3f0059c7f6b3e99a91 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class Main (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index 1ab0351920dad34c674cd5128400a217520e2fad..90cef7831bcf4cff2f87739839a417c8338bf683 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class MapReclaimer (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index 563048ac439602c6668d99309fbce57bc65cd7ad..94a40f100c0c4cf479b30ec9c46797afbc1baa25 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class ScreenshotReclaimer (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index 2e14f09bab2fbcc847a43b2090aa5af2634ab28c..75278bdd1146c50f4ec563ff92b570a4ceb1d359 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Class TerminalUI (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="../stylesheet.css" title="Style">
 <script type="text/javascript" src="../script.js"></script>
 </head>
index 9e1353911a304584e9be5c8b566508f232aa062c..ea15a6ea5793f836ae5999551183f2d9ea847ef7 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Constant Field Values (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
 <ul class="blockList">
 <li class="blockList">
 <table class="constantsSummary" border="0" cellpadding="3" cellspacing="0" summary="Constant Field Values table, listing constant fields, and values">
+<caption><span><a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></span><span class="tabEnd">&nbsp;</span></caption>
+<tr>
+<th class="colFirst" scope="col">Modifier and Type</th>
+<th scope="col">Constant Field</th>
+<th class="colLast" scope="col">Value</th>
+</tr>
+<tbody>
+<tr class="altColor">
+<td class="colFirst"><a name="Arbiter.MAPS_DIR">
+<!--   -->
+</a><code>public&nbsp;static&nbsp;final&nbsp;java.lang.String</code></td>
+<td><code><a href="Arbiter.html#MAPS_DIR">MAPS_DIR</a></code></td>
+<td class="colLast"><code>"maps-reclaimed"</code></td>
+</tr>
+<tr class="rowColor">
+<td class="colFirst"><a name="Arbiter.SCREENSHOTS_DIR">
+<!--   -->
+</a><code>public&nbsp;static&nbsp;final&nbsp;java.lang.String</code></td>
+<td><code><a href="Arbiter.html#SCREENSHOTS_DIR">SCREENSHOTS_DIR</a></code></td>
+<td class="colLast"><code>"screenshots-reclaimed"</code></td>
+</tr>
+</tbody>
+</table>
+</li>
+<li class="blockList">
+<table class="constantsSummary" border="0" cellpadding="3" cellspacing="0" summary="Constant Field Values table, listing constant fields, and values">
 <caption><span><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;">ErrorHandler</a></span><span class="tabEnd">&nbsp;</span></caption>
 <tr>
 <th class="colFirst" scope="col">Modifier and Type</th>
index 9110a93730678b2b3e35fe042e67a8d2543bd61c..dd1a2bd99be53e670c35a534b820701e9c393963 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Deprecated List (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index b6b61fa5bfc99d2845a0d00b294b299dc14ca14b..1bde3bc88eabc331a8eeb20d1491a7474a3cf61b 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>API Help (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 44be75869e7337dc82ac1db102d9c33d3358d5d9..447e6e5b53432fc404bc5bc13f6e1d3e1fc49bc1 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Index (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -70,7 +70,7 @@
 <!--   -->
 </a></div>
 <!-- ========= END OF TOP NAVBAR ========= -->
-<div class="contentContainer"><a href="#I:A">A</a>&nbsp;<a href="#I:B">B</a>&nbsp;<a href="#I:C">C</a>&nbsp;<a href="#I:E">E</a>&nbsp;<a href="#I:G">G</a>&nbsp;<a href="#I:M">M</a>&nbsp;<a href="#I:N">N</a>&nbsp;<a href="#I:P">P</a>&nbsp;<a href="#I:S">S</a>&nbsp;<a href="#I:T">T</a>&nbsp;<a href="#I:U">U</a>&nbsp;<a name="I:A">
+<div class="contentContainer"><a href="#I:A">A</a>&nbsp;<a href="#I:B">B</a>&nbsp;<a href="#I:C">C</a>&nbsp;<a href="#I:D">D</a>&nbsp;<a href="#I:E">E</a>&nbsp;<a href="#I:F">F</a>&nbsp;<a href="#I:G">G</a>&nbsp;<a href="#I:M">M</a>&nbsp;<a href="#I:N">N</a>&nbsp;<a href="#I:P">P</a>&nbsp;<a href="#I:S">S</a>&nbsp;<a href="#I:T">T</a>&nbsp;<a href="#I:U">U</a>&nbsp;<a name="I:A">
 <!--   -->
 </a>
 <h2 class="title">A</h2>
 <dl>
 <dt><span class="memberNameLink"><a href="ErrorHandler.html#CANT_PROCESS">CANT_PROCESS</a></span> - Static variable in class <a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;">ErrorHandler</a></dt>
 <dd>&nbsp;</dd>
+<dt><span class="memberNameLink"><a href="Cortana.html#copyHalo3Files-java.io.File-">copyHalo3Files(File)</a></span> - Static method in class <a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></dt>
+<dd>
+<div class="block">Copies every Halo 3 file from the stick into a new temporary folder and
+ returns that folder.</div>
+</dd>
+<dt><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Cortana</span></a> - Class in <a href="package-summary.html">&lt;Unnamed&gt;</a></dt>
+<dd>
+<div class="block">Reader for USB sticks set up by the Xbox 360.</div>
+</dd>
+<dt><span class="memberNameLink"><a href="Cortana.html#Cortana--">Cortana()</a></span> - Constructor for class <a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></dt>
+<dd>&nbsp;</dd>
+</dl>
+<a name="I:D">
+<!--   -->
+</a>
+<h2 class="title">D</h2>
+<dl>
+<dt><span class="memberNameLink"><a href="Cortana.html#deleteCopies-java.io.File-">deleteCopies(File)</a></span> - Static method in class <a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></dt>
+<dd>
+<div class="block">Deletes the temporary folder made by copyHalo3Files().</div>
+</dd>
 </dl>
 <a name="I:E">
 <!--   -->
 <div class="block">TUI only: print error AND QUIT</div>
 </dd>
 <dt><span class="memberNameLink"><a href="MapReclaimer.html#extractMap-java.io.File-java.io.File-">extractMap(File, File)</a></span> - Method in class <a href="MapReclaimer.html" title="class in &lt;Unnamed&gt;">MapReclaimer</a></dt>
-<dd>&nbsp;</dd>
+<dd>
+<div class="block">Extracts the Forge map from an Xbox 360 file and saves it as
+ &lt;input name&gt;-reclaimed.map in the output directory.</div>
+</dd>
 <dt><span class="memberNameLink"><a href="ScreenshotReclaimer.html#extractScreenshot-java.io.File-java.io.File-">extractScreenshot(File, File)</a></span> - Method in class <a href="ScreenshotReclaimer.html" title="class in &lt;Unnamed&gt;">ScreenshotReclaimer</a></dt>
 <dd>
 <div class="block">Extracts a screenshot from an Xbox 360 file and saves it as a JPEG.</div>
 </dd>
 </dl>
+<a name="I:F">
+<!--   -->
+</a>
+<h2 class="title">F</h2>
+<dl>
+<dt><span class="memberNameLink"><a href="Cortana.html#findStorage-java.io.File-">findStorage(File)</a></span> - Static method in class <a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></dt>
+<dd>
+<div class="block">Returns the Xbox360 folder when the input is a USB stick set up by the
+ console, its Xbox360 folder or one of its Data files.</div>
+</dd>
+</dl>
 <a name="I:G">
 <!--   -->
 </a>
 <h2 class="title">G</h2>
 <dl>
-<dt><span class="memberNameLink"><a href="Arbiter.html#getFileTypeDescription-java.io.File-">getFileTypeDescription(File)</a></span> - Static method in class <a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></dt>
-<dd>&nbsp;</dd>
 <dt><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">GraphicalUI</span></a> - Class in <a href="package-summary.html">&lt;Unnamed&gt;</a></dt>
 <dd>
 <div class="block">Graphical User Interface for Halo 3 Reclaimer.</div>
 </dd>
 <dt><span class="memberNameLink"><a href="MapReclaimer.html#MapReclaimer--">MapReclaimer()</a></span> - Constructor for class <a href="MapReclaimer.html" title="class in &lt;Unnamed&gt;">MapReclaimer</a></dt>
 <dd>&nbsp;</dd>
+<dt><span class="memberNameLink"><a href="Arbiter.html#MAPS_DIR">MAPS_DIR</a></span> - Static variable in class <a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></dt>
+<dd>&nbsp;</dd>
 </dl>
 <a name="I:N">
 <!--   -->
 <h2 class="title">P</h2>
 <dl>
 <dt><span class="memberNameLink"><a href="Arbiter.html#processFile-java.io.File-java.io.File-">processFile(File, File)</a></span> - Static method in class <a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></dt>
-<dd>&nbsp;</dd>
+<dd>
+<div class="block">Reclaims all screenshots and maps found in a file or folder.</div>
+</dd>
 <dt><span class="memberNameLink"><a href="TerminalUI.html#processFile-java.lang.String:A-">processFile(String[])</a></span> - Static method in class <a href="TerminalUI.html" title="class in &lt;Unnamed&gt;">TerminalUI</a></dt>
 <dd>
 <div class="block">processes file extraction from command line.</div>
 </dd>
 <dt><span class="memberNameLink"><a href="ScreenshotReclaimer.html#ScreenshotReclaimer--">ScreenshotReclaimer()</a></span> - Constructor for class <a href="ScreenshotReclaimer.html" title="class in &lt;Unnamed&gt;">ScreenshotReclaimer</a></dt>
 <dd>&nbsp;</dd>
+<dt><span class="memberNameLink"><a href="Arbiter.html#SCREENSHOTS_DIR">SCREENSHOTS_DIR</a></span> - Static variable in class <a href="Arbiter.html" title="class in &lt;Unnamed&gt;">Arbiter</a></dt>
+<dd>&nbsp;</dd>
 <dt><span class="memberNameLink"><a href="TerminalUI.html#showHelp--">showHelp()</a></span> - Static method in class <a href="TerminalUI.html" title="class in &lt;Unnamed&gt;">TerminalUI</a></dt>
 <dd>&nbsp;</dd>
 <dt><span class="memberNameLink"><a href="TerminalUI.html#showTooManyArguments--">showTooManyArguments()</a></span> - Static method in class <a href="TerminalUI.html" title="class in &lt;Unnamed&gt;">TerminalUI</a></dt>
 <dt><span class="memberNameLink"><a href="ErrorHandler.html#UNKNOWN_FILE">UNKNOWN_FILE</a></span> - Static variable in class <a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;">ErrorHandler</a></dt>
 <dd>&nbsp;</dd>
 </dl>
-<a href="#I:A">A</a>&nbsp;<a href="#I:B">B</a>&nbsp;<a href="#I:C">C</a>&nbsp;<a href="#I:E">E</a>&nbsp;<a href="#I:G">G</a>&nbsp;<a href="#I:M">M</a>&nbsp;<a href="#I:N">N</a>&nbsp;<a href="#I:P">P</a>&nbsp;<a href="#I:S">S</a>&nbsp;<a href="#I:T">T</a>&nbsp;<a href="#I:U">U</a>&nbsp;</div>
+<a href="#I:A">A</a>&nbsp;<a href="#I:B">B</a>&nbsp;<a href="#I:C">C</a>&nbsp;<a href="#I:D">D</a>&nbsp;<a href="#I:E">E</a>&nbsp;<a href="#I:F">F</a>&nbsp;<a href="#I:G">G</a>&nbsp;<a href="#I:M">M</a>&nbsp;<a href="#I:N">N</a>&nbsp;<a href="#I:P">P</a>&nbsp;<a href="#I:S">S</a>&nbsp;<a href="#I:T">T</a>&nbsp;<a href="#I:U">U</a>&nbsp;</div>
 <!-- ======= START OF BOTTOM NAVBAR ====== -->
 <div class="bottomNav"><a name="navbar.bottom">
 <!--   -->
index 61486f1ab69546a98b1f3ddd4c292ff301663ae5..37a8f0c06092b1978f5df12241b3f8a7a01b64e5 100644 (file)
@@ -2,7 +2,7 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Halo 3 Reclaimer</title>
 <script type="text/javascript">
     tmpTargetPage = "" + window.location.search;
index ed7c0e6003f1fece176fa70767a8b4fb3e8dc8f7..5890b6392f608167ab79ec4977da6f626bf32e45 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Overview (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 5ae38f8b208a567e3c097a30c4a64ae89acd9821..e0bc245b81544245adafbb39f143d10d124bc805 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:49 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Class Hierarchy (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
 </li>
 </ul>
 </li>
+<li type="circle"><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Cortana</span></a></li>
 <li type="circle"><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">ErrorHandler</span></a></li>
 <li type="circle"><a href="Main.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Main</span></a></li>
 <li type="circle"><a href="MapReclaimer.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">MapReclaimer</span></a></li>
index 43d5aa97dda1666da2b5fb31bbbae26a376f1e5e..962c54bb0c08dbff3bf0c278923520e15a909ffc 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>&lt;Unnamed&gt; (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
@@ -14,6 +14,7 @@
 <h2 title="Classes">Classes</h2>
 <ul title="Classes">
 <li><a href="Arbiter.html" title="class in &lt;Unnamed&gt;" target="classFrame">Arbiter</a></li>
+<li><a href="Cortana.html" title="class in &lt;Unnamed&gt;" target="classFrame">Cortana</a></li>
 <li><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;" target="classFrame">ErrorHandler</a></li>
 <li><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;" target="classFrame">GraphicalUI</a></li>
 <li><a href="Main.html" title="class in &lt;Unnamed&gt;" target="classFrame">Main</a></li>
index 545f662a04d6d44530910a39aaa2cb637835eaa1..e8c9a87ecbaef56cdd78a8b4d5751252b0ee7bf1 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title> (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
 </td>
 </tr>
 <tr class="rowColor">
+<td class="colFirst"><a href="Cortana.html" title="class in &lt;Unnamed&gt;">Cortana</a></td>
+<td class="colLast">
+<div class="block">Reader for USB sticks set up by the Xbox 360.</div>
+</td>
+</tr>
+<tr class="altColor">
 <td class="colFirst"><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;">ErrorHandler</a></td>
 <td class="colLast">
 <div class="block">Simple error handling class.</div>
 </td>
 </tr>
-<tr class="altColor">
+<tr class="rowColor">
 <td class="colFirst"><a href="GraphicalUI.html" title="class in &lt;Unnamed&gt;">GraphicalUI</a></td>
 <td class="colLast">
 <div class="block">Graphical User Interface for Halo 3 Reclaimer.</div>
 </td>
 </tr>
-<tr class="rowColor">
+<tr class="altColor">
 <td class="colFirst"><a href="Main.html" title="class in &lt;Unnamed&gt;">Main</a></td>
 <td class="colLast">
 <div class="block">Main entry point for the program.</div>
 </td>
 </tr>
-<tr class="altColor">
+<tr class="rowColor">
 <td class="colFirst"><a href="MapReclaimer.html" title="class in &lt;Unnamed&gt;">MapReclaimer</a></td>
 <td class="colLast">
 <div class="block">Core extraction engine for Forge map variants.</div>
 </td>
 </tr>
-<tr class="rowColor">
+<tr class="altColor">
 <td class="colFirst"><a href="ScreenshotReclaimer.html" title="class in &lt;Unnamed&gt;">ScreenshotReclaimer</a></td>
 <td class="colLast">
 <div class="block">Core extraction engine for screenshots.</div>
 </td>
 </tr>
-<tr class="altColor">
+<tr class="rowColor">
 <td class="colFirst"><a href="TerminalUI.html" title="class in &lt;Unnamed&gt;">TerminalUI</a></td>
 <td class="colLast">
 <div class="block">Terminal User Interface for Halo 3 Reclaimer.</div>
index 47b96cb9dd76ccb243b873cdfe737aa3eb2e4d24..e1b8c80dd58dbe51184c4fae1ccc90c441088633 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title> Class Hierarchy (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
 </li>
 </ul>
 </li>
+<li type="circle"><a href="Cortana.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Cortana</span></a></li>
 <li type="circle"><a href="ErrorHandler.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">ErrorHandler</span></a></li>
 <li type="circle"><a href="Main.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">Main</span></a></li>
 <li type="circle"><a href="MapReclaimer.html" title="class in &lt;Unnamed&gt;"><span class="typeNameLink">MapReclaimer</span></a></li>
index 9ac8125da936583287f030f0f42bd234c8582395..72967b08109e582b1e9d9d722b560705f311f9c9 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Uses of Package  (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index e00571dfe31696adcc867ebaf5fc3405a8586eae..98c2f06ecb3920ede340952fadb1e2fb0092fe2a 100644 (file)
@@ -2,9 +2,9 @@
 <!-- NewPage -->
 <html lang="en">
 <head>
-<!-- Generated by javadoc (1.8.0_432) on Sat Jul 19 01:19:48 CEST 2025 -->
+<!-- Generated by javadoc (1.8.0_504) on Mon Nov 02 17:45:23 CET 2026 -->
 <title>Serialized Form (Halo 3 Reclaimer)</title>
-<meta name="date" content="2025-07-19">
+<meta name="date" content="2026-11-02">
 <link rel="stylesheet" type="text/css" href="stylesheet.css" title="Style">
 <script type="text/javascript" src="script.js"></script>
 </head>
index 1f2e60895b91491e1430a641e7d390ea9798dc9b..9bcb026b8884efed1316449c8b029db39c6f61ff 100644 (file)
Binary files a/doc/user-manual-editable.odt and b/doc/user-manual-editable.odt differ
index 8f832a632eaa3c0eded90c552c27e2740711c722..4a98f7ccb0e8b11ad168b21cbda7abbf0c0f6629 100644 (file)
Binary files a/doc/user-manual.pdf and b/doc/user-manual.pdf differ
index dae1f0d2b0c5f053e80bd4e5cdf0bfc4cb234a95..990df864af721065a4f3c02acfd0655caccc141b 100644 (file)
Binary files a/halo3-reclaimer.jar and b/halo3-reclaimer.jar differ
index 6fa6706bdcdfd0749e78dfac920bb42c468c05ca..b260dc4a9ed0584cfdfbd0fda9e2e16446d9fcbd 100644 (file)
 /*
 * Halo 3 Reclaimer [Arbiter.java]
-* player data extractor from Xbox 360 version of Halo 3 
-* copyright 2025, David Polakovic, licensed under GPLv3
+* player data extractor from Xbox 360 version of Halo 3
+* copyright 2025, 2026, David Polakovic, licensed under GPLv3
 * contact: email@dpolakovic.space
 */
 
+import java.io.DataInputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
 * Arbiter class that determines file type and routes to appropriate extractor.
-* 
-* <p>This class analyzes the input file to determine whether it's a screenshot
-* or map file, then calls the appropriate reclaimer. The Xbox 360 saves file 
-* type directly in file name. So it just checks for names...</p>
-* 
-* @version 1.0
+*
+* <p>The input is a single file, a whole folder, or a USB stick set up by the
+* Xbox 360. A stick (or its Xbox360 folder, or one of its Data files) goes to
+* Cortana first, which copies the Halo 3 files out of the Xbox file system;
+* other files on the stick are ignored then.</p>
+*
+* <p>Halo 3 keeps every screenshot and every Forge map in its own package,
+* so the Arbiter looks inside each file: a package holding
+* <code>screen.shot</code> is a screenshot, one holding <code>sandbox.map</code>
+* is a map. Screenshots are checked first, then maps. Anything else is skipped.
+* Only when the content says nothing (DDS, TGA, raw textures) the file name
+* decides, like in version 1.0.</p>
+*
+* <p>Results go to two folders in the output directory:
+* <code>screenshots-reclaimed</code> and <code>maps-reclaimed</code>.</p>
+*
+* @version 1.1
 */
 
 public class Arbiter {
-    
-    public static void processFile(File inputFile, File outputDir) throws Exception {
-        String fileName = inputFile.getName().toLowerCase();
-        
-        if (isScreenshotFile(fileName)) {
-            // route to screenshot extractor
-            ScreenshotReclaimer screenshotReclaimer = new ScreenshotReclaimer();
-            screenshotReclaimer.extractScreenshot(inputFile, outputDir);
-        } else if (isMapFile(fileName)) {
-            // route to map extractor
-            MapReclaimer mapReclaimer = new MapReclaimer();
-            mapReclaimer.extractMap(inputFile, outputDir);
-        } else {
-            // unrecognized file type
-            throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE);
-        }
-    }
-    
+
+    // folders created inside the output directory
+    public static final String SCREENSHOTS_DIR = "screenshots-reclaimed";
+    public static final String MAPS_DIR = "maps-reclaimed";
+
+    // the file inside a package that holds a screenshot
+    private static final String SCREENSHOT_FILE = "screen.shot";
+
+    // packages are small, bigger files are not worth reading
+    private static final long MAX_FILE_SIZE = 16L * 1024 * 1024;
+
+    // how deep to look into sub folders
+    private static final int MAX_FOLDER_DEPTH = 8;
+
+    private enum Kind { SCREENSHOT, MAP, OTHER }
+
+    /**
+    * Reclaims all screenshots and maps found in a file or folder.
+    *
+    * @return what was reclaimed, for example "Reclaimed 2 pictures and 1 map"
+    * @throws Exception if nothing could be reclaimed
+    */
+    public static String processFile(File input, File outputDir) throws Exception {
+        // a USB stick set up by the console: Cortana gets the Halo 3 files out of it first
+        File storage = Cortana.findStorage(input);
+        if (storage != null) {
+            File copies = Cortana.copyHalo3Files(storage);
+            try {
+                return reclaim(listFiles(copies), outputDir, true, copies);
+            } finally {
+                Cortana.deleteCopies(copies);
+            }
+        }
+
+        if (input.isDirectory()) {
+            return reclaim(listFiles(input), outputDir, true, input);
+        }
+        List<File> files = new ArrayList<File>();
+        files.add(input);
+        return reclaim(files, outputDir, false, null);
+    }
+
+    /**
+    * Sorts the files out and runs the right reclaimer on each, screenshots first.
+    * folder: there are many files to go through, root: where console paths start.
+    */
+    private static String reclaim(List<File> files, File outputDir, boolean folder, File root) throws Exception {
+        // sort everything out first, screenshots go first
+        List<File> screenshots = new ArrayList<File>();
+        List<File> maps = new ArrayList<File>();
+        String skipReason = null;
+        for (File file : files) {
+            StringBuilder reason = new StringBuilder();
+            StringBuilder detail = new StringBuilder();
+            Kind kind = detect(file, reason, detail);
+            if (kind == Kind.SCREENSHOT) {
+                screenshots.add(file);
+            } else if (kind == Kind.MAP) {
+                maps.add(file);
+            } else {
+                // short reason for the status bar, the detail only goes to the console
+                skipReason = reason.toString();
+                if (folder) {
+                    System.out.println("Skipping " + shown(root, file) + " ("
+                            + (detail.length() > 0 ? detail : skipReason) + ")");
+                } else if (detail.length() > 0) {
+                    System.out.println("Not reclaimed: " + detail);
+                }
+            }
+        }
+
+        if (screenshots.isEmpty() && maps.isEmpty()) {
+            if (folder) {
+                throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE + ": no Halo 3 files found");
+            }
+            throw ErrorHandler.makeException(skipReason);
+        }
+        if (folder) {
+            System.out.println("Found " + count(screenshots.size(), "screenshot")
+                    + " and " + count(maps.size(), "map"));
+        }
+
+        int failed = 0;
+        Exception lastError = null;
+
+        int pictures = 0;
+        if (!screenshots.isEmpty()) {
+            File screenshotsDir = outputFolder(outputDir, SCREENSHOTS_DIR);
+            for (File file : screenshots) {
+                System.out.println("Detected as screenshot: " + shown(root, file));
+                try {
+                    new ScreenshotReclaimer().extractScreenshot(file, screenshotsDir);
+                    pictures++;
+                } catch (Exception e) {
+                    failed++;
+                    lastError = e;
+                    System.out.println("Could not reclaim " + shown(root, file) + ": " + e.getMessage());
+                }
+            }
+        }
+
+        int reclaimedMaps = 0;
+        if (!maps.isEmpty()) {
+            File mapsDir = outputFolder(outputDir, MAPS_DIR);
+            for (File file : maps) {
+                System.out.println("Detected as Forge map: " + shown(root, file));
+                try {
+                    new MapReclaimer().extractMap(file, mapsDir);
+                    reclaimedMaps++;
+                } catch (Exception e) {
+                    failed++;
+                    lastError = e;
+                    System.out.println("Could not reclaim " + shown(root, file) + ": " + e.getMessage());
+                }
+            }
+        }
+
+        if (pictures + reclaimedMaps == 0) {
+            if (failed == 1) {
+                throw lastError;
+            }
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": all " + failed + " files failed");
+        }
+
+        List<String> reclaimed = new ArrayList<String>();
+        if (pictures > 0) reclaimed.add(count(pictures, "picture"));
+        if (reclaimedMaps > 0) reclaimed.add(count(reclaimedMaps, "map"));
+        String result = "Reclaimed " + String.join(" and ", reclaimed);
+        if (failed > 0) {
+            result += " (" + failed + " failed)";
+        }
+        return result;
+    }
+
+    /**
+    * Tells what a file is by looking inside it. Screenshots are checked first.
+    * When the file is neither, a short error goes into reason and, when
+    * there is more to say, an explanation for the console into detail.
+    */
+    private static Kind detect(File file, StringBuilder reason, StringBuilder detail) {
+        if (file.length() > MAX_FILE_SIZE) {
+            reason.append(ErrorHandler.CANT_PROCESS + ": file too big");
+            detail.append(file.length() + " bytes, too big for a screenshot or map");
+            return Kind.OTHER;
+        }
+
+        byte[] data;
+        try {
+            data = readFile(file);
+        } catch (IOException e) {
+            reason.append(ErrorHandler.BAD_INPUT + ": " + e.getMessage());
+            return Kind.OTHER;
+        }
+
+        if (MapReclaimer.isSTFSFile(data)) {
+            try {
+                List<String> names = MapReclaimer.listPackageFiles(data);
+                if (containsName(names, SCREENSHOT_FILE)) return Kind.SCREENSHOT;
+                if (containsName(names, MapReclaimer.MAP_VARIANT_FILE)) return Kind.MAP;
+                // game types, films...
+                reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot or map");
+                detail.append(names.isEmpty() ? "the package is empty"
+                        : "the package holds " + String.join(", ", names));
+                return Kind.OTHER;
+            } catch (Exception e) {
+                // another game or a damaged package, the name may still tell
+                reason.append(e.getMessage() != null ? e.getMessage() : ErrorHandler.CANT_PROCESS);
+            }
+        } else if (MapReclaimer.isBLFFile(data)) {
+            // screen.shot or sandbox.map already taken out of its package
+            int fileType = MapReclaimer.readBlfFileType(data);
+            if (fileType == MapReclaimer.FILE_TYPE_SCREENSHOT) return Kind.SCREENSHOT;
+            if (fileType == MapReclaimer.FILE_TYPE_MAP_VARIANT) return Kind.MAP;
+            if (fileType >= 0) {
+                reason.append(ErrorHandler.CANT_PROCESS + ": not a screenshot or map");
+                detail.append("Halo file of type " + fileType + " (game type, film...)");
+                return Kind.OTHER;
+            }
+        }
+
+        // the content says nothing (DDS, TGA, raw textures), let the name decide
+        String fileName = file.getName().toLowerCase();
+        if (isScreenshotFile(fileName)) return Kind.SCREENSHOT;
+        if (isMapFile(fileName)) return Kind.MAP;
+
+        if (reason.length() == 0) {
+            reason.append(ErrorHandler.UNKNOWN_FILE);
+        }
+        return Kind.OTHER;
+    }
 
     private static boolean isScreenshotFile(String fileName) {
         return fileName.contains("screenshot") || fileName.contains("shot");
     }
-    
 
     private static boolean isMapFile(String fileName) {
         return fileName.contains("map") || fileName.contains("variant") || fileName.contains("forge");
     }
-    
-    public static String getFileTypeDescription(File inputFile) {
-        String fileName = inputFile.getName().toLowerCase();
-        
-        if (isScreenshotFile(fileName)) {
-            return "Screenshot file";
-        } else if (isMapFile(fileName)) {
-            return "Map file";
-        } else {
-            return "Unrecognized file";
+
+    private static boolean containsName(List<String> names, String wanted) {
+        for (String name : names) {
+            if (name.equalsIgnoreCase(wanted)) return true;
+        }
+        return false;
+    }
+
+    private static List<File> listFiles(File folder) {
+        List<File> files = new ArrayList<File>();
+        collectFiles(folder, files, 0);
+        return files;
+    }
+
+    // path for the console, starting at the picked folder
+    private static String shown(File root, File file) {
+        if (root == null) return file.getPath();
+        try {
+            return root.toPath().relativize(file.toPath()).toString();
+        } catch (IllegalArgumentException e) {
+            return file.getPath();
         }
     }
-}
\ No newline at end of file
+
+    /**
+    * Adds all files of a folder and its sub folders, skipping our own output folders.
+    */
+    private static void collectFiles(File folder, List<File> files, int depth) {
+        if (depth > MAX_FOLDER_DEPTH) return;
+        File[] children = folder.listFiles();
+        if (children == null) return;
+        Arrays.sort(children);
+        for (File child : children) {
+            if (child.isDirectory()) {
+                String name = child.getName();
+                if (!name.equals(SCREENSHOTS_DIR) && !name.equals(MAPS_DIR)) {
+                    collectFiles(child, files, depth + 1);
+                }
+            } else if (child.isFile()) {
+                files.add(child);
+            }
+        }
+    }
+
+    /**
+    * Returns the output sub folder, creates it when needed.
+    */
+    private static File outputFolder(File outputDir, String name) throws Exception {
+        File folder = new File(outputDir, name);
+        if (!folder.isDirectory() && !folder.mkdirs()) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": " + folder.getPath());
+        }
+        return folder;
+    }
+
+    private static byte[] readFile(File file) throws IOException {
+        byte[] data = new byte[(int) file.length()];
+        try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+            in.readFully(data);
+        }
+        return data;
+    }
+
+    // "1 map", "2 maps"
+    private static String count(int number, String word) {
+        return number + " " + word + (number == 1 ? "" : "s");
+    }
+}
diff --git a/src/Cortana.java b/src/Cortana.java
new file mode 100644 (file)
index 0000000..ac32a1e
--- /dev/null
@@ -0,0 +1,454 @@
+/*
+* Halo 3 Reclaimer [Cortana.java]
+* player data extractor from Xbox 360 version of Halo 3
+* copyright 2026, David Polakovic, licensed under GPLv3
+* contact: email@dpolakovic.space
+*/
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+* Reader for USB sticks set up by the Xbox 360.
+*
+* <p>The console doesn't store saves on the stick as normal files. It creates
+* a hidden folder <code>Xbox360</code> with big files <code>Data0000</code>,
+* <code>Data0001</code>, <code>Data0002</code>... Joined together they are one
+* virtual drive in the Xbox's own FATX file system. Cortana opens that drive,
+* walks to the Halo 3 folders (<code>Content\&lt;profile&gt;\4D5307E6\...</code>)
+* and copies their files into a temporary folder, where the Arbiter sorts them
+* out like any other files. Nothing on the stick is ever changed.</p>
+*
+* <p>Only Cortana can understand these needlesly complicated Michaelsoft file
+* systems.</p>
+*
+* <p>Partition offsets and the FATX layout follow Free60 and Velocity
+* (hetelek/Velocity, GPLv3).</p>
+*
+* @version 1.0
+*/
+
+public class Cortana {
+
+    // the hidden folder the console creates on the stick
+    private static final String STORAGE_FOLDER = "Xbox360";
+
+    // Halo 3 keeps its files in folders named after its title ID
+    private static final String HALO3_FOLDER = "4D5307E6";
+
+    // user content starts 512 MB into the joined Data files
+    private static final long CONTENT_PARTITION = 0x20000000L;
+
+    // FATX format constants
+    private static final int FATX_MAGIC = 0x58544146; // "XTAF"
+    private static final int FATX_HEADER_SIZE = 0x1000; // the allocation table follows it
+    private static final int ENTRY_SIZE = 0x40;
+    private static final int ENTRY_NAME_LENGTH = 0x2A;
+    private static final int ENTRY_DELETED = 0xE5;
+    private static final int ATTRIBUTE_DIRECTORY = 0x10;
+
+    // screenshots and maps are small, add-ons and updates stay on the stick
+    private static final long MAX_FILE_SIZE = 16L * 1024 * 1024;
+
+    // how deep to look for the Halo 3 folders, and how long a folder may get
+    private static final int MAX_DEPTH = 6;
+    private static final int MAX_FOLDER_CLUSTERS = 1024;
+
+    /**
+    * Returns the Xbox360 folder when the input is a USB stick set up by the
+    * console, its Xbox360 folder or one of its Data files. Otherwise null.
+    */
+    public static File findStorage(File input) {
+        if (input == null) return null;
+
+        if (input.isFile()) {
+            // one of the Data files was picked
+            File parent = input.getParentFile();
+            if (input.getName().matches("(?i)data\\d{4}") && parent != null && hasDataFiles(parent)) {
+                return parent;
+            }
+            return null;
+        }
+
+        if (input.isDirectory()) {
+            if (hasDataFiles(input)) return input;
+            File storage = findChild(input, STORAGE_FOLDER);
+            if (storage != null && storage.isDirectory() && hasDataFiles(storage)) {
+                return storage;
+            }
+        }
+        return null;
+    }
+
+    /**
+    * Copies every Halo 3 file from the stick into a new temporary folder and
+    * returns that folder. Remove it with deleteCopies() when done.
+    *
+    * @throws Exception if the stick can't be read
+    */
+    public static File copyHalo3Files(File storage) throws Exception {
+        List<File> dataFiles = listDataFiles(storage);
+        System.out.println("Reading Xbox 360 USB storage: " + storage.getPath()
+                + " (" + dataFiles.size() + " Data files)");
+
+        File copies;
+        try {
+            copies = Files.createTempDirectory("halo3-reclaimer-").toFile();
+        } catch (IOException e) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": no temporary folder");
+        }
+
+        try (Drive drive = new Drive(dataFiles)) {
+            if (drive.length <= CONTENT_PARTITION + FATX_HEADER_SIZE) {
+                throw unreadable("the Data files hold only " + drive.length + " bytes");
+            }
+            Volume content = new Volume(drive, CONTENT_PARTITION, drive.length - CONTENT_PARTITION, true);
+            int copied = copyFolder(content, content.root(), copies, "", false, 0);
+            System.out.println("Copied " + copied + " Halo 3 file(s) off the stick");
+        } catch (RuntimeException e) {
+            // a bug or a layout nobody has seen yet
+            deleteCopies(copies);
+            e.printStackTrace();
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": USB stick not readable");
+        } catch (IOException e) {
+            deleteCopies(copies);
+            throw ErrorHandler.makeException(ErrorHandler.BAD_INPUT + ": " + e.getMessage());
+        } catch (Exception e) {
+            deleteCopies(copies);
+            throw e;
+        }
+        return copies;
+    }
+
+    /**
+    * Deletes the temporary folder made by copyHalo3Files().
+    */
+    public static void deleteCopies(File folder) {
+        if (folder == null) return;
+        File[] children = folder.listFiles();
+        if (children != null) {
+            for (File child : children) {
+                if (child.isDirectory()) {
+                    deleteCopies(child);
+                } else {
+                    child.delete();
+                }
+            }
+        }
+        folder.delete();
+    }
+
+    /**
+    * Walks the folders. Inside a 4D5307E6 folder every file is copied out.
+    */
+    private static int copyFolder(Volume volume, Entry folder, File target, String path,
+                                  boolean inHalo3, int depth) throws Exception {
+        if (depth > MAX_DEPTH) return 0;
+
+        int copied = 0;
+        for (Entry entry : volume.list(folder)) {
+            String entryPath = path.isEmpty() ? entry.name : path + "\\" + entry.name;
+            File entryTarget = new File(target, safeName(entry.name));
+
+            if (entry.isDirectory()) {
+                boolean halo3 = inHalo3 || entry.name.equalsIgnoreCase(HALO3_FOLDER);
+                if (halo3 && !inHalo3) {
+                    System.out.println("Found Halo 3 folder: " + entryPath);
+                }
+                copied += copyFolder(volume, entry, entryTarget, entryPath, halo3, depth + 1);
+            } else if (inHalo3) {
+                if (entry.size > MAX_FILE_SIZE) {
+                    System.out.println("Leaving " + entryPath + " on the stick (" + entry.size
+                            + " bytes, too big for a screenshot or map)");
+                    continue;
+                }
+                writeFile(entryTarget, volume.read(entry));
+                copied++;
+            }
+        }
+        return copied;
+    }
+
+    /**
+    * Data0000, Data0001... in order, until one is missing.
+    */
+    private static List<File> listDataFiles(File storage) {
+        List<File> files = new ArrayList<File>();
+        for (int i = 0; i < 10000; i++) {
+            File data = findChild(storage, String.format("Data%04d", i));
+            if (data == null || !data.isFile()) break;
+            files.add(data);
+        }
+        return files;
+    }
+
+    private static boolean hasDataFiles(File folder) {
+        File first = findChild(folder, "Data0000");
+        return first != null && first.isFile();
+    }
+
+    /**
+    * Finds a file or folder by name, ignoring upper and lower case.
+    */
+    private static File findChild(File folder, String name) {
+        File exact = new File(folder, name);
+        if (exact.exists()) return exact;
+        String[] names = folder.list();
+        if (names == null) return null;
+        for (String candidate : names) {
+            if (candidate.equalsIgnoreCase(name)) return new File(folder, candidate);
+        }
+        return null;
+    }
+
+    private static void writeFile(File file, byte[] data) throws Exception {
+        File parent = file.getParentFile();
+        if (parent != null && !parent.isDirectory() && !parent.mkdirs()) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": no temporary folder");
+        }
+        try (FileOutputStream out = new FileOutputStream(file)) {
+            out.write(data);
+        } catch (IOException e) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": " + e.getMessage());
+        }
+    }
+
+    // Xbox names are plain, but never trust them with the PC's file system
+    private static String safeName(String name) {
+        String safe = name.replaceAll("[\\\\/:*?\"<>|\\p{Cntrl}]", "_").trim();
+        return (safe.isEmpty() || safe.equals(".") || safe.equals("..")) ? "_" : safe;
+    }
+
+    // short message for the status bar, the reason goes to the console
+    private static Exception damaged(String reason) {
+        System.out.println("Damaged USB stick: " + reason);
+        return ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged USB stick");
+    }
+
+    private static Exception unreadable(String reason) {
+        System.out.println("USB stick not readable: " + reason);
+        return ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": USB stick not readable");
+    }
+
+    private static int readInt32(byte[] data, int offset) {
+        return ((data[offset] & 0xFF) << 24) | ((data[offset + 1] & 0xFF) << 16)
+                | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF);
+    }
+
+    /**
+    * One file or folder on the Xbox drive.
+    */
+    private static final class Entry {
+        String name;
+        int attributes;
+        long firstCluster;
+        long size;
+
+        boolean isDirectory() {
+            return (attributes & ATTRIBUTE_DIRECTORY) != 0;
+        }
+    }
+
+    /**
+    * The Data files joined into one long drive. Only the needed parts are read,
+    * the files can be gigabytes.
+    */
+    private static final class Drive implements Closeable {
+        private final RandomAccessFile[] files;
+        private final long[] starts;
+        private final long[] lengths;
+        final long length;
+
+        Drive(List<File> dataFiles) throws IOException {
+            files = new RandomAccessFile[dataFiles.size()];
+            starts = new long[files.length];
+            lengths = new long[files.length];
+            long total = 0;
+            try {
+                for (int i = 0; i < files.length; i++) {
+                    files[i] = new RandomAccessFile(dataFiles.get(i), "r");
+                    starts[i] = total;
+                    lengths[i] = files[i].length();
+                    total += lengths[i];
+                }
+            } catch (IOException e) {
+                close();
+                throw e;
+            }
+            length = total;
+        }
+
+        /**
+        * Reads count bytes at a position of the joined drive.
+        */
+        byte[] read(long position, int count) throws Exception {
+            if (position < 0 || count < 0 || position + count > length) {
+                throw damaged("something points past the end of the Data files");
+            }
+            byte[] out = new byte[count];
+            int done = 0;
+            int index = 0;
+            while (done < count) {
+                long pos = position + done;
+                while (pos >= starts[index] + lengths[index]) {
+                    index++;
+                }
+                long inFile = pos - starts[index];
+                int chunk = (int) Math.min(count - done, lengths[index] - inFile);
+                files[index].seek(inFile);
+                files[index].readFully(out, done, chunk);
+                done += chunk;
+            }
+            return out;
+        }
+
+        @Override
+        public void close() {
+            for (RandomAccessFile file : files) {
+                if (file != null) {
+                    try {
+                        file.close();
+                    } catch (IOException e) {
+                        // nothing left to do
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+    * One FATX partition: a 4 KB header, the allocation table, then the clusters.
+    */
+    private static final class Volume {
+        private final Drive drive;
+        private final long rootCluster;
+        private final int clusterSize;
+        private final int fatEntrySize; // 2 or 4 bytes
+        private final long fatStart;
+        private final long clusterStart;
+        private final long clusterCount;
+
+        Volume(Drive drive, long start, long size, boolean usbContent) throws Exception {
+            this.drive = drive;
+
+            // header: magic, partition ID, sectors per cluster, root folder cluster
+            byte[] header = drive.read(start, 16);
+            if (readInt32(header, 0) != FATX_MAGIC) {
+                throw unreadable("no Xbox file system where the content should start");
+            }
+            int sectorsPerCluster = readInt32(header, 8);
+            rootCluster = readInt32(header, 12) & 0xFFFFFFFFL;
+            switch (sectorsPerCluster) {
+                case 0x2: case 0x4: case 0x8: case 0x10: case 0x20: case 0x40: case 0x80:
+                    break;
+                default:
+                    throw damaged("unknown cluster size");
+            }
+            clusterSize = sectorsPerCluster << 9;
+            int shift = Integer.numberOfTrailingZeros(clusterSize);
+
+            // size of the allocation table, worked out the same way as the console does;
+            // the content partition of a USB stick always uses 4 byte entries
+            long clusters = (size >> shift) + 1;
+            int entryShift = (usbContent || clusters >= 0xFFF0) ? 2 : 1;
+            long fatSize = ((clusters << entryShift) + 0xFFF) & ~0xFFFL & 0xFFFFFFFFL;
+            long rest = size - FATX_HEADER_SIZE;
+            if (rest < fatSize) {
+                throw damaged("partition too small for its allocation table");
+            }
+
+            fatEntrySize = 1 << entryShift;
+            fatStart = start + FATX_HEADER_SIZE;
+            clusterStart = fatStart + fatSize;
+            clusterCount = (rest - fatSize) >> shift;
+        }
+
+        Entry root() {
+            Entry root = new Entry();
+            root.name = "";
+            root.attributes = ATTRIBUTE_DIRECTORY;
+            root.firstCluster = rootCluster;
+            return root;
+        }
+
+        /**
+        * Files and folders inside a folder, deleted ones left out.
+        */
+        List<Entry> list(Entry folder) throws Exception {
+            List<Entry> entries = new ArrayList<Entry>();
+            long cluster = folder.firstCluster;
+            int visited = 0;
+            while (isCluster(cluster)) {
+                if (++visited > MAX_FOLDER_CLUSTERS) {
+                    throw damaged("a folder never ends");
+                }
+                byte[] data = drive.read(clusterOffset(cluster), clusterSize);
+                for (int pos = 0; pos + ENTRY_SIZE <= data.length; pos += ENTRY_SIZE) {
+                    // entry: name length, attributes, 42 byte name, first cluster, size, 3 dates
+                    int nameLength = data[pos] & 0xFF;
+                    if (nameLength == 0x00 || nameLength == 0xFF) {
+                        return entries; // end of the folder
+                    }
+                    if (nameLength == ENTRY_DELETED || nameLength > ENTRY_NAME_LENGTH) {
+                        continue; // deleted
+                    }
+                    Entry entry = new Entry();
+                    entry.attributes = data[pos + 1] & 0xFF;
+                    entry.name = new String(data, pos + 2, nameLength, StandardCharsets.ISO_8859_1);
+                    entry.firstCluster = readInt32(data, pos + 0x2C) & 0xFFFFFFFFL;
+                    entry.size = readInt32(data, pos + 0x30) & 0xFFFFFFFFL;
+                    entries.add(entry);
+                }
+                cluster = nextCluster(cluster);
+            }
+            return entries;
+        }
+
+        /**
+        * The contents of a file, following its cluster chain.
+        */
+        byte[] read(Entry file) throws Exception {
+            byte[] out = new byte[(int) file.size];
+            long cluster = file.firstCluster;
+            int done = 0;
+            while (done < out.length) {
+                if (!isCluster(cluster)) {
+                    throw damaged("a file is cut off");
+                }
+                int chunk = Math.min(clusterSize, out.length - done);
+                byte[] part = drive.read(clusterOffset(cluster), chunk);
+                System.arraycopy(part, 0, out, done, chunk);
+                done += chunk;
+                if (done < out.length) {
+                    cluster = nextCluster(cluster);
+                }
+            }
+            return out;
+        }
+
+        // clusters count from 1; free (0) and end markers fall outside this range
+        private boolean isCluster(long cluster) {
+            return cluster >= 1 && cluster <= clusterCount;
+        }
+
+        private long clusterOffset(long cluster) {
+            return clusterStart + (cluster - 1) * clusterSize;
+        }
+
+        // the allocation table holds the next cluster of every cluster
+        private long nextCluster(long cluster) throws Exception {
+            byte[] entry = drive.read(fatStart + cluster * fatEntrySize, fatEntrySize);
+            if (fatEntrySize == 4) {
+                return readInt32(entry, 0) & 0xFFFFFFFFL;
+            }
+            return ((entry[0] & 0xFF) << 8) | (entry[1] & 0xFF);
+        }
+    }
+}
index f51ba1be0b11b945035533dd4399c47c7f78f206..08112f6c675803f52c3818400af2c21b1c75f6a8 100644 (file)
@@ -1,7 +1,7 @@
 /*
 * Halo 3 Reclaimer [GraphicalUI.java]
 * player data extractor from Xbox 360 version of Halo 3 
-* copyright 2025, David Polakovic, licensed under GPLv3
+* copyright 2025, 2026, David Polakovic, licensed under GPLv3
 * contact: email@dpolakovic.space
 */
 
@@ -17,7 +17,7 @@ import java.io.File;
 * <p>This class contains all the GUI components and 
 * logic for the application's windowed interface</p>
 * 
-* @version 1.0
+* @version 1.1
 */
 
 public class GraphicalUI extends JFrame {
@@ -104,7 +104,7 @@ public class GraphicalUI extends JFrame {
         // the action button
         JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
         buttonPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
-        reclaimButton = new JButton("PELICAN!");
+        reclaimButton = new JButton("reclaim");
         reclaimButton.setPreferredSize(new Dimension(120, 40));
         reclaimButton.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 14));
         buttonPanel.add(reclaimButton);
@@ -161,9 +161,9 @@ public class GraphicalUI extends JFrame {
     */
     private void showVersionInfo() {
         String versionText = "Halo 3 Reclaimer\n\n" +
-                           "Version: 1.0\n\n" +
+                           "Version: 0.2\n\n" +
                            "player data extractor from Xbox 360 version of Halo 3\n" +
-                           "copyright 2025, David Polakovic, licensed under GPLv3\n";
+                           "copyright 2025, 2026, David Polakovic, licensed under GPLv3\n";
         
         JOptionPane.showMessageDialog(this, versionText, "Version Information", 
                                     JOptionPane.INFORMATION_MESSAGE);
@@ -178,11 +178,11 @@ public class GraphicalUI extends JFrame {
                         "   to select your Xbox 360 Halo 3 file\n\n" +
                         "2. Click 'Browse' next to 'Save destination'\n" +
                         "   to choose where to save the converted image\n\n" +
-                        "3. Click 'PELICAN!' to start the extraction process\n\n" +
-                        "The tool will convert your Xbox 360 file\n" +
-                        "into a standard JPEG image or map file*\n" +
-                         "that you share on any device.\n\n" +
-                         "* map files are not implemented yet";
+                        "3. Click 'reclaim' to start the extraction process\n\n" +
+                        "The tool will extract your user data from the\n" +
+                       "Xbox 360 save. Currently supports:\n" +
+                       "-screenshots\n" +
+                       "-Forge maps\n\n";
         
         JOptionPane.showMessageDialog(this, helpText, "Help", 
                                     JOptionPane.INFORMATION_MESSAGE);
@@ -226,8 +226,9 @@ public class GraphicalUI extends JFrame {
         @Override
         public void actionPerformed(ActionEvent e) {
             JFileChooser fileChooser = new JFileChooser();
-            fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
-            fileChooser.setDialogTitle("Select Halo 3 raw file");
+            fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
+            fileChooser.setFileHidingEnabled(false); // the Xbox360 folder on a stick is hidden
+            fileChooser.setDialogTitle("Select Halo 3 file or folder");
             
             // set file filter for Xbox file formats
             fileChooser.addChoosableFileFilter(new javax.swing.filechooser.FileFilter() {
@@ -318,36 +319,28 @@ public class GraphicalUI extends JFrame {
             resetStatusColor();
             setStatus("Processing file...");
             
-            SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
+            SwingWorker<String, Void> worker = new SwingWorker<String, Void>() {
                 @Override
-                protected Void doInBackground() throws Exception {
+                protected String doInBackground() throws Exception {
                     try {
-                        Arbiter.processFile(rawFile, saveDir);
+                        return Arbiter.processFile(rawFile, saveDir);
                     } catch (Exception ex) {
                         SwingUtilities.invokeLater(() -> {
                             setErrorStatus(ex.getMessage());
                         });
                         throw ex; 
                     }
-                    return null;
                 }
                 
                 @Override
                 protected void done() {
                     reclaimButton.setEnabled(true);
-                    reclaimButton.setText("PELICAN!");
+                    reclaimButton.setText("reclaim");
                     
                     try {
-                        get(); // Check if any exception occurred
-                        
-                        String baseName = rawFile.getName();
-                        int dotIndex = baseName.lastIndexOf('.');
-                        if (dotIndex > 0) {
-                            baseName = baseName.substring(0, dotIndex);
-                        }
-                        String outputFileName = baseName + "-reclaimed.jpg";
-                        
-                        setSuccessStatus("Success! Saved as: " + outputFileName);
+                        // e.g. "Reclaimed 2 pictures and 1 map", throws if any exception occurred
+                        String result = get();
+                        setSuccessStatus("Success! " + result);
                     } catch (Exception ex) { }
                 }
             };
@@ -355,4 +348,4 @@ public class GraphicalUI extends JFrame {
             worker.execute();
         }
     }
-}
\ No newline at end of file
+}
index c95c256035855cd57a9d213066e48276ed8383ad..9a136c670048fc6826454647cb81b7bb1c3c6577 100644 (file)
 /*
 * Halo 3 Reclaimer [MapReclaimer.java]
-* player data extractor from Xbox 360 version of Halo 3 
-* copyright 2025, David Polakovic, licensed under GPLv3
+* player data extractor from Xbox 360 version of Halo 3
+* copyright 2025, 2026, David Polakovic, licensed under GPLv3
 * contact: email@dpolakovic.space
 */
 
+import java.io.DataInputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 
 /**
 * Core extraction engine for Forge map variants.
-* 
-* <p>This is harder than it looks. WIP.</p>
-* 
-* @version "1.0"
+*
+* <p>Halo 3 saves every Forge map as its own Xbox 360 STFS package
+* (a CON file with a long hex name). The package holds one file called
+* <code>sandbox.map</code>. That file is the Forge map variant itself:
+* the object placements on top of one of the original maps, stored in
+* Bungie's BLF format (chunks <code>_blf</code>, <code>chdr</code>,
+* <code>mvar</code> and <code>_eof</code>).</p>
+*
+* <p>Unlike screenshots, the map has no magic bytes worth scanning for,
+* so this class reads the STFS file system properly: it walks the file
+* table, follows the block chain of <code>sandbox.map</code> and checks
+* each block against the package hash tables.</p>
+*
+* <p>Supported input formats:</p>
+* <ul>
+*   <li>STFS container files (CON, LIVE, PIRS) holding a Halo 3 Forge map</li>
+*   <li>already extracted <code>sandbox.map</code> files (BLF)</li>
+* </ul>
+*
+* <p>The saved file is not converted in any way. It is byte for byte the
+* <code>sandbox.map</code> the console keeps inside the package.</p>
+*
+* @version 1.0
 */
 
 public class MapReclaimer {
-    
 
+    // every Halo 3 package carries this title ID in its header
+    private static final int HALO3_TITLE_ID = 0x4D5307E6;
+
+    // a Forge map package is around 100 KB, anything this big is something else
+    private static final long MAX_INPUT_SIZE = 16L * 1024 * 1024;
+
+    // STFS format constants
+    private static final int STFS_MAGIC_CON = 0x434F4E20; // "CON "
+    private static final int STFS_MAGIC_LIVE = 0x4C495645; // "LIVE"
+    private static final int STFS_MAGIC_PIRS = 0x50495253; // "PIRS"
+    private static final int STFS_BLOCK_SIZE = 0x1000;
+    private static final int STFS_BLOCKS_PER_TABLE = 0xAA; // data blocks covered by one hash table
+    private static final int STFS_BLOCKS_PER_LEVEL1 = 0x70E4; // data blocks covered by one level 1 table
+    private static final int STFS_HASH_ENTRY_SIZE = 0x18;
+    private static final int STFS_FILE_ENTRY_SIZE = 0x40;
+
+    // the file inside the package that holds the Forge map
+    static final String MAP_VARIANT_FILE = "sandbox.map";
+
+    // BLF format constants (the format of sandbox.map)
+    private static final int BLF_START = 0x5F626C66; // "_blf"
+    private static final int BLF_END = 0x5F656F66; // "_eof"
+    private static final int BLF_CONTENT_HEADER = 0x63686472; // "chdr"
+    private static final int BLF_MAP_VARIANT = 0x6D766172; // "mvar", Xbox 360
+    private static final int BLF_MAP_VARIANT_PC = 0x6D617076; // "mapv", PC builds of the engine
+    private static final int BLF_CHUNK_HEADER_SIZE = 12;
+
+    // item metadata (name, author...) sits 0x10 bytes into both chdr and mvar,
+    // these offsets are relative to its start
+    private static final int METADATA_OFFSET = 0x10;
+    private static final int METADATA_NAME = 0x08; // UTF-16, 16 characters
+    private static final int METADATA_DESCRIPTION = 0x28; // UTF-8, 128 bytes
+    private static final int METADATA_AUTHOR = 0xA8; // 16 bytes
+    private static final int METADATA_FILE_TYPE = 0xB8;
+    private static final int METADATA_MAP_ID = 0xE0;
+
+    // saved game file types as stored in the metadata
+    static final int FILE_TYPE_MAP_VARIANT = 10;
+    static final int FILE_TYPE_SCREENSHOT = 13;
+    private static final String[] FILE_TYPE_NAMES = {
+        "campaign save", "Capture the Flag game type", "Slayer game type",
+        "Oddball game type", "King of the Hill game type", "Juggernaut game type",
+        "Territories game type", "Assault game type", "Infection game type",
+        "VIP game type", "Forge map", "saved film", "film clip", "screenshot"
+    };
+
+    /**
+    * Extracts the Forge map from an Xbox 360 file and saves it as
+    * &lt;input name&gt;-reclaimed.map in the output directory.
+    *
+    * @throws Exception if the file cannot be read, is not a Halo 3 Forge map,
+    *                   or is damaged
+    */
     public void extractMap(File inputFile, File outputDir) throws Exception {
-        throw ErrorHandler.makeException(ErrorHandler.NOT_READY);
+        byte[] fileData = readFile(inputFile);
+
+        System.out.println("File size: " + fileData.length + " bytes");
+
+        byte[] mapData;
+        MapInfo info;
+        try {
+            if (isSTFSFile(fileData)) {
+                System.out.println("Detected STFS file format");
+                mapData = extractFromSTFS(fileData);
+            } else if (isBLFFile(fileData)) {
+                System.out.println("Detected extracted map variant (BLF) file");
+                mapData = fileData;
+            } else {
+                System.out.println("Not an Xbox 360 package or map variant");
+                throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a Forge map");
+            }
+            info = readMapInfo(mapData);
+        } catch (RuntimeException e) {
+            // a bug or a file layout nobody has seen yet, keep the GUI informed
+            e.printStackTrace();
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged or unsupported file");
+        }
+
+        System.out.println("Forge map: \"" + info.name + "\"");
+        if (!info.description.isEmpty()) {
+            System.out.println("Description: " + info.description);
+        }
+        if (!info.author.isEmpty()) {
+            System.out.println("Author: " + info.author);
+        }
+        if (info.mapId >= 0) {
+            System.out.println("Base map ID: " + info.mapId);
+        }
+        System.out.println("Map variant size: " + mapData.length + " bytes");
+
+        // generate output filename, same scheme as the screenshots
+        String baseName = inputFile.getName();
+        int dotIndex = baseName.lastIndexOf('.');
+        if (dotIndex > 0) {
+            baseName = baseName.substring(0, dotIndex);
+        }
+        String outputFileName = baseName + "-reclaimed.map";
+        File outputFile = new File(outputDir, outputFileName);
+
+        writeFile(outputFile, mapData);
+        System.out.println("Saved as: " + outputFile.getAbsolutePath());
+    }
+
+    /**
+    * Reads the entire contents of a file into a byte array.
+    */
+    private byte[] readFile(File file) throws Exception {
+        long length = file.length();
+        if (length > MAX_INPUT_SIZE) {
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": file too big for a Forge map");
+        }
+
+        byte[] data = new byte[(int) length];
+        try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
+            in.readFully(data);
+        } catch (IOException e) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_INPUT + ": " + e.getMessage());
+        }
+        return data;
+    }
+
+    private void writeFile(File file, byte[] data) throws Exception {
+        try (FileOutputStream out = new FileOutputStream(file)) {
+            out.write(data);
+        } catch (IOException e) {
+            throw ErrorHandler.makeException(ErrorHandler.BAD_OUTPUT + ": " + e.getMessage());
+        }
+    }
+
+    static boolean isSTFSFile(byte[] data) {
+        if (data.length < 4) return false;
+        int magic = readInt32(data, 0);
+        return magic == STFS_MAGIC_CON || magic == STFS_MAGIC_LIVE || magic == STFS_MAGIC_PIRS;
+    }
+
+    static boolean isBLFFile(byte[] data) {
+        return data.length >= BLF_CHUNK_HEADER_SIZE && readInt32(data, 0) == BLF_START;
+    }
+
+    /**
+    * Lists the files inside a Halo 3 package. The Arbiter uses this to tell
+    * screenshots (screen.shot) from maps (sandbox.map) without trusting file names.
+    *
+    * @throws Exception if the package is damaged or belongs to another game
+    */
+    static List<String> listPackageFiles(byte[] data) throws Exception {
+        StfsPackage stfs = new StfsPackage(data);
+        if (stfs.titleId != HALO3_TITLE_ID) {
+            System.out.printf("Package belongs to title %08X, not Halo 3%n", stfs.titleId);
+            throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE + ": not a Halo 3 package");
+        }
+        List<String> names = new ArrayList<String>();
+        for (StfsEntry entry : stfs.entries) {
+            if (!entry.isDirectory) {
+                names.add(entry.name);
+            }
+        }
+        return names;
+    }
+
+    /**
+    * Saved game file type of an already extracted BLF file
+    * (FILE_TYPE_SCREENSHOT, FILE_TYPE_MAP_VARIANT...), or -1 if it can't be told.
+    */
+    static int readBlfFileType(byte[] blf) {
+        if (!isBLFFile(blf)) return -1;
+        int[] chunks = findBlfChunks(blf);
+        if (chunks == null) return -1;
+        if (hasMetadata(blf, chunks[0])) {
+            return readInt32(blf, chunks[0] + METADATA_OFFSET + METADATA_FILE_TYPE);
+        }
+        return chunks[1] >= 0 ? FILE_TYPE_MAP_VARIANT : -1;
+    }
+
+    /**
+    * Finds sandbox.map in the package and returns its contents.
+    */
+    private byte[] extractFromSTFS(byte[] data) throws Exception {
+        StfsPackage stfs = new StfsPackage(data);
+
+        System.out.printf("Package: \"%s\", title ID %08X%n", stfs.displayName, stfs.titleId);
+        if (stfs.titleId != HALO3_TITLE_ID) {
+            throw ErrorHandler.makeException(ErrorHandler.UNKNOWN_FILE + ": not a Halo 3 package");
+        }
+
+        StfsEntry mapEntry = null;
+        for (StfsEntry entry : stfs.entries) {
+            System.out.println("Package contains: " + entry.name
+                    + (entry.isDirectory ? " (folder)" : " (" + entry.size + " bytes)"));
+            if (mapEntry == null && !entry.isDirectory && entry.name.equalsIgnoreCase(MAP_VARIANT_FILE)) {
+                mapEntry = entry;
+            }
+        }
+
+        if (mapEntry == null) {
+            // the "Package contains" lines above tell what is in there instead
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": no Forge map inside");
+        }
+
+        byte[] mapData = stfs.readFile(mapEntry);
+
+        if (stfs.badBlocks > 0) {
+            System.out.println("Warning: " + stfs.badBlocks + " block(s) of " + MAP_VARIANT_FILE
+                    + " do not match the package hashes, the package may be damaged");
+        } else {
+            System.out.println("Package hashes OK");
+        }
+        return mapData;
+    }
+
+    /**
+    * Walks the BLF chunks of a map variant, makes sure it really is a Forge map
+    * and reads its name, description and author.
+    */
+    private MapInfo readMapInfo(byte[] blf) throws Exception {
+        if (!isBLFFile(blf)) {
+            System.out.println(MAP_VARIANT_FILE + " is not a map variant (BLF) file");
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a Forge map");
+        }
+
+        int[] chunks = findBlfChunks(blf);
+        if (chunks == null) {
+            System.out.println("Map variant has a broken chunk");
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged map variant");
+        }
+        int contentHeader = chunks[0];
+        int mapVariant = chunks[1];
+        if (chunks[2] < 0) {
+            System.out.println("Map variant is incomplete, its end of file chunk is missing");
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged map variant");
+        }
+
+        // both chdr and mvar start with the same metadata block, chdr is the official one
+        int metadata = -1;
+        if (hasMetadata(blf, contentHeader)) {
+            metadata = contentHeader + METADATA_OFFSET;
+        } else if (hasMetadata(blf, mapVariant)) {
+            metadata = mapVariant + METADATA_OFFSET;
+        }
+
+        int fileType = metadata >= 0 ? readInt32(blf, metadata + METADATA_FILE_TYPE) : -1;
+        if (mapVariant < 0 && fileType != FILE_TYPE_MAP_VARIANT) {
+            if (fileType >= 0 && fileType < FILE_TYPE_NAMES.length) {
+                System.out.println("This file is a " + FILE_TYPE_NAMES[fileType]);
+            }
+            throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": not a Forge map");
+        }
+
+        MapInfo info = new MapInfo();
+        if (metadata >= 0) {
+            info.name = readUtf16(blf, metadata + METADATA_NAME, 32);
+            info.description = readUtf8(blf, metadata + METADATA_DESCRIPTION, 128);
+            info.author = readUtf8(blf, metadata + METADATA_AUTHOR, 16);
+            info.mapId = readInt32(blf, metadata + METADATA_MAP_ID);
+        }
+        return info;
+    }
+
+    /**
+    * Walks the BLF chunks (4 byte signature, 4 byte size including the header,
+    * 2+2 byte version) and returns the offsets of the chdr chunk, the map variant
+    * chunk and the _eof chunk, -1 for the missing ones. Null if a chunk is broken.
+    */
+    private static int[] findBlfChunks(byte[] blf) {
+        int contentHeader = -1;
+        int mapVariant = -1;
+        int end = -1;
+
+        int offset = 0;
+        while (offset + BLF_CHUNK_HEADER_SIZE <= blf.length) {
+            int signature = readInt32(blf, offset);
+            int size = readInt32(blf, offset + 4);
+            if (size < BLF_CHUNK_HEADER_SIZE || size > blf.length - offset) {
+                return null;
+            }
+
+            if (signature == BLF_CONTENT_HEADER && contentHeader < 0) {
+                contentHeader = offset;
+            } else if ((signature == BLF_MAP_VARIANT || signature == BLF_MAP_VARIANT_PC) && mapVariant < 0) {
+                mapVariant = offset;
+            } else if (signature == BLF_END) {
+                end = offset;
+                break;
+            }
+            offset += size;
+        }
+        return new int[] { contentHeader, mapVariant, end };
+    }
+
+    /**
+    * True if there is a chunk at chunkOffset big enough to hold the metadata block.
+    */
+    private static boolean hasMetadata(byte[] blf, int chunkOffset) {
+        if (chunkOffset < 0) return false;
+        int chunkSize = readInt32(blf, chunkOffset + 4);
+        return chunkSize >= METADATA_OFFSET + METADATA_MAP_ID + 4;
+    }
+
+    /**
+    * What the map variant says about itself.
+    */
+    private static final class MapInfo {
+        String name = "";
+        String description = "";
+        String author = "";
+        int mapId = -1;
+    }
+
+    /**
+    * One entry of the STFS file table.
+    */
+    private static final class StfsEntry {
+        final String name;
+        final boolean isDirectory;
+        final boolean consecutive; // blocks follow each other, no need for the hash chain
+        final int blockCount;
+        final int startBlock;
+        final int size;
+
+        StfsEntry(byte[] data, int offset) {
+            int flags = data[offset + 0x28] & 0xFF;
+            int nameLength = Math.min(flags & 0x3F, 0x28);
+            int end = offset;
+            while (end < offset + nameLength && data[end] != 0) {
+                end++;
+            }
+            name = new String(data, offset, end - offset, StandardCharsets.ISO_8859_1);
+            isDirectory = (flags & 0x80) != 0;
+            consecutive = (flags & 0x40) != 0;
+            blockCount = readUInt24LE(data, offset + 0x29);
+            startBlock = readUInt24LE(data, offset + 0x2F);
+            size = readInt32(data, offset + 0x34);
+        }
+    }
+
+    /**
+    * Read only STFS reader, just enough to list a package and read files from it.
+    *
+    * <p>Data blocks are 4 KB. Every 170 data blocks there is a hash table
+    * (and above 170 blocks also a level 1 table). Packages made by the console
+    * keep two copies of each table and a status bit says which one is current.
+    * Block math follows Free60 and Velocity (hetelek/Velocity).</p>
+    */
+    private static final class StfsPackage {
+        final byte[] data;
+        final int titleId;
+        final String displayName;
+        final List<StfsEntry> entries = new ArrayList<StfsEntry>();
+        int badBlocks = 0;
+
+        private final int blockSeparation;
+        private final int tableShift; // 1 = two copies of every hash table, 0 = one copy
+        private final int allocatedBlocks;
+        private final long firstHashTable;
+        private final int topLevel;
+        private final long topTable;
+
+        StfsPackage(byte[] data) throws Exception {
+            this.data = data;
+
+            if (data.length < 0x3AD) {
+                throw damaged("header is cut off");
+            }
+            int headerSize = readInt32(data, 0x340);
+            if (headerSize < 0x3AD || headerSize > data.length) {
+                throw damaged("bad header size");
+            }
+            titleId = readInt32(data, 0x360);
+            displayName = readUtf16(data, 0x411, 0x80);
+
+            // volume descriptor at 0x379, file system type at 0x3A9 (0 = STFS)
+            if ((data[0x379] & 0xFF) != 0x24 || readInt32(data, 0x3A9) != 0) {
+                System.out.println("Package does not use the STFS file system");
+                throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": unsupported package type");
+            }
+            blockSeparation = data[0x37B] & 0xFF;
+            int fileTableBlockCount = readUInt16LE(data, 0x37C);
+            int fileTableBlock = readUInt24LE(data, 0x37E);
+            allocatedBlocks = readInt32(data, 0x395);
+
+            tableShift = (~blockSeparation) & 1;
+            firstHashTable = (headerSize + 0xFFFL) & ~0xFFFL;
+
+            if (allocatedBlocks <= 0) {
+                throw damaged("no data blocks");
+            } else if (allocatedBlocks <= STFS_BLOCKS_PER_TABLE) {
+                topLevel = 0;
+            } else if (allocatedBlocks <= STFS_BLOCKS_PER_LEVEL1) {
+                topLevel = 1;
+            } else {
+                System.out.println("Package has " + allocatedBlocks + " blocks, far more than a Forge map needs");
+                throw ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": file too big for a Forge map");
+            }
+
+            // the top table is the level 0 table for small packages, the level 1 table otherwise
+            long topTableBlock = (topLevel == 0) ? 0 : levelOneTableBlock();
+            topTable = firstHashTable + (topTableBlock << 12) + secondCopyOffset((blockSeparation & 2) != 0);
+
+            readFileTable(fileTableBlock, fileTableBlockCount);
+        }
+
+        /**
+        * Reads all file entries, following the block chain of the file table.
+        */
+        private void readFileTable(int block, int blockCount) throws Exception {
+            for (int i = 0; i < blockCount; i++) {
+                long base = blockOffset(checkBlock(block));
+                requireBytes(base, STFS_BLOCK_SIZE);
+
+                for (int slot = 0; slot < STFS_BLOCK_SIZE / STFS_FILE_ENTRY_SIZE; slot++) {
+                    int entry = (int) base + slot * STFS_FILE_ENTRY_SIZE;
+                    int flags = data[entry + 0x28] & 0xFF;
+                    if ((flags & 0x3F) == 0) continue; // empty slot
+                    if (data[entry] == 0) break; // end of the listing
+                    entries.add(new StfsEntry(data, entry));
+                }
+
+                if (i + 1 < blockCount) {
+                    block = nextBlock(block);
+                }
+            }
+        }
+
+        /**
+        * Returns the contents of a file in the package.
+        */
+        byte[] readFile(StfsEntry entry) throws Exception {
+            if (entry.isDirectory || entry.size < 0
+                    || (long) entry.size > (long) entry.blockCount * STFS_BLOCK_SIZE) {
+                throw damaged("bad file table entry");
+            }
+
+            byte[] out = new byte[entry.size];
+            int block = entry.startBlock;
+            int copied = 0;
+            for (int i = 0; copied < entry.size; i++) {
+                if (i >= entry.blockCount) {
+                    throw damaged("block chain is too short");
+                }
+                long offset = blockOffset(checkBlock(block));
+                int length = Math.min(STFS_BLOCK_SIZE, entry.size - copied);
+                requireBytes(offset, length);
+                System.arraycopy(data, (int) offset, out, copied, length);
+                verifyBlock(block, offset);
+
+                copied += length;
+                if (copied < entry.size) {
+                    block = entry.consecutive ? block + 1 : nextBlock(block);
+                }
+            }
+            return out;
+        }
+
+        /**
+        * File offset of a data block: skips the header and every hash table in front of it.
+        * Valid below 0x70E4 blocks, bigger packages are rejected in the constructor.
+        */
+        private long blockOffset(int block) {
+            long backing = block + (((long) block + STFS_BLOCKS_PER_TABLE) / STFS_BLOCKS_PER_TABLE << tableShift);
+            if (block >= STFS_BLOCKS_PER_TABLE) {
+                backing += 1L << tableShift; // the level 1 table(s)
+            }
+            return firstHashTable + (backing << 12);
+        }
+
+        /**
+        * Block number (counting hash tables) of the level 0 hash table covering a data block.
+        */
+        private long levelZeroTableBlock(int block) {
+            if (block < STFS_BLOCKS_PER_TABLE) return 0;
+            long groupSize = STFS_BLOCKS_PER_TABLE + (1L << tableShift);
+            return (block / STFS_BLOCKS_PER_TABLE) * groupSize + (1L << tableShift);
+        }
+
+        /**
+        * Block number (counting hash tables) of the level 1 hash table.
+        */
+        private long levelOneTableBlock() {
+            return STFS_BLOCKS_PER_TABLE + (1L << tableShift);
+        }
+
+        private long secondCopyOffset(boolean useSecondCopy) {
+            return (tableShift == 1 && useSecondCopy) ? STFS_BLOCK_SIZE : 0;
+        }
+
+        /**
+        * File offset of the hash entry that describes a data block.
+        */
+        private long hashEntryOffset(int block) throws Exception {
+            boolean secondCopy;
+            if (topLevel == 0) {
+                secondCopy = (blockSeparation & 2) != 0;
+            } else {
+                // the level 1 entry of this group says which level 0 copy is current
+                secondCopy = (readByte(topTable + (long) (block / STFS_BLOCKS_PER_TABLE) * STFS_HASH_ENTRY_SIZE + 0x14) & 0x40) != 0;
+            }
+            return firstHashTable + (levelZeroTableBlock(block) << 12) + secondCopyOffset(secondCopy)
+                    + (long) (block % STFS_BLOCKS_PER_TABLE) * STFS_HASH_ENTRY_SIZE;
+        }
+
+        /**
+        * Next block of a chain, as written in the hash entry (24 bit, big endian).
+        */
+        private int nextBlock(int block) throws Exception {
+            long entry = hashEntryOffset(block);
+            return (readByte(entry + 0x15) << 16) | (readByte(entry + 0x16) << 8) | readByte(entry + 0x17);
+        }
+
+        /**
+        * Compares the SHA-1 of a data block with the one stored in the hash table.
+        */
+        private void verifyBlock(int block, long offset) throws Exception {
+            long entry = hashEntryOffset(block);
+            if (offset + STFS_BLOCK_SIZE > data.length || entry + 0x14 > data.length) {
+                badBlocks++;
+                return;
+            }
+            MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
+            sha1.update(data, (int) offset, STFS_BLOCK_SIZE);
+            byte[] expected = Arrays.copyOfRange(data, (int) entry, (int) entry + 0x14);
+            if (!Arrays.equals(sha1.digest(), expected)) {
+                badBlocks++;
+            }
+        }
+
+        private int checkBlock(int block) throws Exception {
+            if (block < 0 || block >= allocatedBlocks) {
+                throw damaged("block chain points outside the package");
+            }
+            return block;
+        }
+
+        private void requireBytes(long offset, int length) throws Exception {
+            if (offset < 0 || offset + length > data.length) {
+                throw damaged("file is cut off");
+            }
+        }
+
+        private int readByte(long offset) throws Exception {
+            requireBytes(offset, 1);
+            return data[(int) offset] & 0xFF;
+        }
+
+        // short message for the status bar, the reason goes to the console
+        private static Exception damaged(String reason) {
+            System.out.println("Damaged package: " + reason);
+            return ErrorHandler.makeException(ErrorHandler.CANT_PROCESS + ": damaged package");
+        }
+    }
+
+    // helpers, Xbox 360 data is big endian except a few STFS fields
+
+    private static int readInt32(byte[] data, int offset) {
+        return ((data[offset] & 0xFF) << 24) | ((data[offset + 1] & 0xFF) << 16)
+                | ((data[offset + 2] & 0xFF) << 8) | (data[offset + 3] & 0xFF);
+    }
+
+    private static int readUInt16LE(byte[] data, int offset) {
+        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8);
+    }
+
+    private static int readUInt24LE(byte[] data, int offset) {
+        return (data[offset] & 0xFF) | ((data[offset + 1] & 0xFF) << 8) | ((data[offset + 2] & 0xFF) << 16);
+    }
+
+    /**
+    * Reads a zero terminated UTF-16 (big endian) string of at most maxBytes bytes.
+    */
+    private static String readUtf16(byte[] data, int offset, int maxBytes) {
+        if (offset >= data.length) return "";
+        int limit = Math.min(data.length, offset + maxBytes);
+        int end = offset;
+        while (end + 1 < limit && (data[end] != 0 || data[end + 1] != 0)) {
+            end += 2;
+        }
+        return clean(new String(data, offset, end - offset, StandardCharsets.UTF_16BE));
+    }
+
+    /**
+    * Reads a zero terminated UTF-8 string of at most maxBytes bytes.
+    */
+    private static String readUtf8(byte[] data, int offset, int maxBytes) {
+        if (offset >= data.length) return "";
+        int limit = Math.min(data.length, offset + maxBytes);
+        int end = offset;
+        while (end < limit && data[end] != 0) {
+            end++;
+        }
+        return clean(new String(data, offset, end - offset, StandardCharsets.UTF_8));
+    }
+
+    // drop control characters so a damaged name can't mess up the output
+    private static String clean(String text) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < text.length(); i++) {
+            char c = text.charAt(i);
+            sb.append(Character.isISOControl(c) ? ' ' : c);
+        }
+        return sb.toString().trim();
     }
-}
\ No newline at end of file
+}
index c6c2205ebaec332c551ece9a81597b4e5e0c260c..015adad1e699d111f8baa45c6641bcf693c0e920 100644 (file)
@@ -1,7 +1,7 @@
 /*
 * Halo 3 Reclaimer [TerminalUI.java]
 * player data extractor from Xbox 360 version of Halo 3 
-* copyright 2025, David Polakovic, licensed under GPLv3
+* copyright 2025, 2026, David Polakovic, licensed under GPLv3
 * contact: email@dpolakovic.space
 */
 
@@ -12,7 +12,7 @@ import java.io.File;
 * 
 * <p>This class contains all the command-line interface logic.</p>
 * 
-* @version 1.0
+* @version 1.1
 */
 public class TerminalUI {
     
@@ -85,26 +85,14 @@ public class TerminalUI {
         }
         
         try {
-            System.out.println("Processing file: " + inputFile.getName());
-            
-            String fileType = Arbiter.getFileTypeDescription(inputFile);
-            if (!fileType.equals("Unrecognized file")) {
-                System.out.println("Detected as: " + fileType);
-            }
-            
-            // use Arbiter to process the file
-            Arbiter.processFile(inputFile, outputDir);
-            
-            String baseName = inputFile.getName();
-            int dotIndex = baseName.lastIndexOf('.');
-            if (dotIndex > 0) {
-                baseName = baseName.substring(0, dotIndex);
-            }
-            String outputFileName = baseName + "-reclaimed.jpg";
-            File outputFile = new File(outputDir, outputFileName);
-            
-            System.out.println("File successfully processed to: " + outputFile.getAbsolutePath());
-            
+            System.out.println((inputFile.isDirectory() ? "Processing folder: " : "Processing file: ")
+                    + inputFile.getName());
+
+            // use Arbiter to process the file or folder
+            String result = Arbiter.processFile(inputFile, outputDir);
+
+            System.out.println(result + " into: " + outputDir.getAbsolutePath());
+
         } catch (Exception e) {
             // parse error code from exception message if it starts with a number
             String message = e.getMessage();
@@ -124,4 +112,4 @@ public class TerminalUI {
     public static void showTooManyArguments() {
         ErrorHandler.exitWithError(ErrorHandler.TOO_MANY_ARGS);
     }
-}
\ No newline at end of file
+}
index a9e4591c7aa900ed9d5fa71cdf8749437a3d16b8..16d4afcb1d26ea40c668e0fb853c74922cef34bc 100644 (file)
Binary files a/src/mc.png and b/src/mc.png differ
Copyright 2022-2026 David Polakovic. Individual project licenses are located in project root in full length.

Site generated using Gitweb. Read the documentation for JavaScript and cookie information. Additional source code available here under GPLv3 license.

Server for this subdomain is RFC 2324 compliant